[
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "Makefile.in",
    "content": "package = @PACKAGE_NAME@\nversion = @PACKAGE_VERSION@\nprefix = @prefix@\n# we don't use @exec_prefix@ because it usually contains '${prefix}' literally\n# but we use @prefix@/bin/onedrive in the systemd unit files which are generated\n# from the configure script.\n# Thus, set exec_prefix unconditionally to prefix\n# Alternative approach would be add dep on sed, and do manual generation in the Makefile.\n# exec_prefix = @exec_prefix@\nexec_prefix = @prefix@\ndatarootdir = @datarootdir@\ndatadir = @datadir@\nsrcdir = @srcdir@\nbindir = @bindir@\nmandir = @mandir@\nsysconfdir = @sysconfdir@\ndocdir = $(datadir)/doc/$(package)\nVPATH = @srcdir@\nINSTALL = @INSTALL@\n\n# Icon install locations (system-wide hicolor theme)\nICON_THEMEDIR      = $(datadir)/icons/hicolor\nICON_PLACES_DIR    = $(ICON_THEMEDIR)/scalable/places\nICON_SOURCE_SVG    = contrib/images/onedrive.svg\nICON_TARGET_SVG    = onedrive.svg\n\nNOTIFICATIONS = @NOTIFICATIONS@\nHAVE_SYSTEMD = @HAVE_SYSTEMD@\nsystemduserunitdir = @systemduserunitdir@\nsystemdsystemunitdir = @systemdsystemunitdir@\nall_libs = @curl_LIBS@ @sqlite_LIBS@ @dbus_LIBS@ @notify_LIBS@ @bsd_inotify_LIBS@ @dynamic_linker_LIBS@\nCOMPLETIONS = @COMPLETIONS@\nBASH_COMPLETION_DIR = @BASH_COMPLETION_DIR@\nZSH_COMPLETION_DIR = @ZSH_COMPLETION_DIR@\nFISH_COMPLETION_DIR = @FISH_COMPLETION_DIR@\nDEBUG = @DEBUG@\n\nDC = @DC@\nDCFLAGS = @DCFLAGS@\nDEBUG_DCFLAGS = @DEBUG_DCFLAGS@\nRELEASE_DCFLAGS = @RELEASE_DCFLAGS@\nVERSION_DCFLAG = @VERSION_DCFLAG@\nLINKER_DCFLAG = @LINKER_DCFLAG@\nOUTPUT_DCFLAG = @OUTPUT_DCFLAG@\nWERROR_DCFLAG = @WERROR_DCFLAG@\n\nDCFLAGS += $(WERROR_DCFLAG)\nifeq ($(DEBUG),yes)\nDCFLAGS += $(DEBUG_DCFLAGS)\nelse\nDCFLAGS += $(RELEASE_DCFLAGS)\nendif\n\nifeq ($(NOTIFICATIONS),yes)\nGUI_NOTIFICATIONS = $(addprefix $(VERSION_DCFLAG)=,NoPragma NoGdk Notifications)\nendif\n\nsystem_unit_files = contrib/systemd/onedrive@.service\nuser_unit_files = contrib/systemd/onedrive.service\n\nDOCFILES = 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 docs/server-side-filtering-limitations.md\n\nifneq (\"$(wildcard /etc/redhat-release)\",\"\")\nRHEL = $(shell cat /etc/redhat-release | grep -E \"(Red Hat Enterprise Linux|CentOS|AlmaLinux)\" | wc -l)\nRHEL_VERSION = $(shell rpm --eval \"%{rhel}\")\nelse\nRHEL = 0\nRHEL_VERSION = 0\nendif\n\nSOURCES = \\\n\tsrc/main.d \\\n\tsrc/config.d \\\n\tsrc/log.d \\\n\tsrc/util.d \\\n\tsrc/qxor.d \\\n\tsrc/curlEngine.d \\\n\tsrc/onedrive.d \\\n\tsrc/webhook.d \\\n\tsrc/sync.d \\\n\tsrc/itemdb.d \\\n\tsrc/sqlite.d \\\n\tsrc/clientSideFiltering.d \\\n\tsrc/monitor.d \\\n\tsrc/arsd/cgi.d \\\n\tsrc/xattr.d \\\n\tsrc/intune.d \\\n\tsrc/socketio.d \\\n\tsrc/curlWebsockets.d\n\nifeq ($(NOTIFICATIONS),yes)\nSOURCES += src/notifications/notify.d src/notifications/dnotify.d\nendif\n\nall: onedrive\n\nclean:\n\trm -f onedrive onedrive.o version\n\trm -rf autom4te.cache\n\trm -f config.log config.status\n\n# Remove files generated via ./configure\ndistclean: clean\n\trm -f Makefile contrib/pacman/PKGBUILD contrib/spec/onedrive.spec onedrive.1 $(system_unit_files) $(user_unit_files)\n\nonedrive: $(SOURCES)\n\tif [ -f .git/HEAD ] ; then \\\n\t\tgit describe --tags > version ; \\\n\telse \\\n\t\techo $(version) > version ; \\\n\tfi\n\t$(DC) -J. $(GUI_NOTIFICATIONS) $(DCFLAGS) $^ $(addprefix $(LINKER_DCFLAG),$(all_libs)) $(OUTPUT_DCFLAG)$@\n\ninstall: all\n\tmkdir -p $(DESTDIR)$(bindir)\n\t$(INSTALL) onedrive $(DESTDIR)$(bindir)/onedrive\n\tmkdir -p $(DESTDIR)$(mandir)/man1\n\t$(INSTALL) -m 0644 onedrive.1 $(DESTDIR)$(mandir)/man1/onedrive.1\n\tmkdir -p $(DESTDIR)$(sysconfdir)/logrotate.d\n\t$(INSTALL) -m 0644 contrib/logrotate/onedrive.logrotate $(DESTDIR)$(sysconfdir)/logrotate.d/onedrive\n\tmkdir -p $(DESTDIR)$(docdir)\n\tfor file in $(DOCFILES); do \\\n\t\t$(INSTALL) -m 0644 $$file $(DESTDIR)$(docdir); \\\n\tdone\nifeq ($(HAVE_SYSTEMD),yes)\n\tmkdir -p $(DESTDIR)$(systemduserunitdir)\n\tmkdir -p $(DESTDIR)$(systemdsystemunitdir)\nifeq ($(RHEL),1)\n\t$(INSTALL) -m 0644 $(system_unit_files) $(DESTDIR)$(systemdsystemunitdir)\n\t$(INSTALL) -m 0644 $(user_unit_files) $(DESTDIR)$(systemdsystemunitdir)\nelse\n\t$(INSTALL) -m 0644 $(system_unit_files) $(DESTDIR)$(systemdsystemunitdir)\n\t$(INSTALL) -m 0644 $(user_unit_files) $(DESTDIR)$(systemduserunitdir)\nendif\nelse\nifeq ($(RHEL_VERSION),6)\n\t$(INSTALL) contrib/init.d/onedrive.init $(DESTDIR)/etc/init.d/onedrive\n\t$(INSTALL) contrib/init.d/onedrive_service.sh $(DESTDIR)$(bindir)/onedrive_service.sh\nendif\nendif\nifeq ($(COMPLETIONS),yes)\n\tmkdir -p $(DESTDIR)$(ZSH_COMPLETION_DIR)\n\t$(INSTALL) -m 0644 contrib/completions/complete.zsh $(DESTDIR)$(ZSH_COMPLETION_DIR)/_onedrive\n\tmkdir -p $(DESTDIR)$(BASH_COMPLETION_DIR)\n\t$(INSTALL) -m 0644 contrib/completions/complete.bash $(DESTDIR)$(BASH_COMPLETION_DIR)/onedrive\n\tmkdir -p $(DESTDIR)$(FISH_COMPLETION_DIR)\n\t$(INSTALL) -m 0644 contrib/completions/complete.fish $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish\nendif\n\t# --- OneDrive folder icon (hicolor) ---\n\tmkdir -p $(DESTDIR)$(ICON_PLACES_DIR)\n\t$(INSTALL) -m 0644 $(ICON_SOURCE_SVG) $(DESTDIR)$(ICON_PLACES_DIR)/$(ICON_TARGET_SVG)\n\t# Refresh icon cache only when installing to the live system (not during staged DESTDIR installs)\n\t# and only if the theme directory is a proper theme (has index.theme)\n\tif [ -z \"$(DESTDIR)\" ] && command -v gtk-update-icon-cache >/dev/null 2>&1 \\\n\t   && [ -f \"$(ICON_THEMEDIR)/index.theme\" ]; then \\\n\t\tgtk-update-icon-cache -q \"$(ICON_THEMEDIR)\"; \\\n\tfi\n\n\nuninstall:\n\trm -f $(DESTDIR)$(bindir)/onedrive\n\trm -f $(DESTDIR)$(mandir)/man1/onedrive.1\n\trm -f $(DESTDIR)$(sysconfdir)/logrotate.d/onedrive\nifeq ($(HAVE_SYSTEMD),yes)\nifeq ($(RHEL),1)\n\trm -f $(DESTDIR)$(systemdsystemunitdir)/onedrive*.service\nelse\n\trm -f $(DESTDIR)$(systemdsystemunitdir)/onedrive*.service\n\trm -f $(DESTDIR)$(systemduserunitdir)/onedrive*.service\nendif\nelse\nifeq ($(RHEL_VERSION),6)\n\trm -f $(DESTDIR)/etc/init.d/onedrive\n\trm -f $(DESTDIR)$(bindir)/onedrive_service.sh\nendif\nendif\n\tfor i in $(DOCFILES) ; do rm -f $(DESTDIR)$(docdir)/$$i ; done\nifeq ($(COMPLETIONS),yes)\n\trm -f $(DESTDIR)$(ZSH_COMPLETION_DIR)/_onedrive\n\trm -f $(DESTDIR)$(BASH_COMPLETION_DIR)/onedrive\n\trm -f $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish\nendif\n\t# --- OneDrive folder icon (hicolor) ---\n\trm -f $(DESTDIR)$(ICON_PLACES_DIR)/$(ICON_TARGET_SVG)\n\t# Refresh icon cache if removing from the live system and index.theme exists\n\tif [ -z \"$(DESTDIR)\" ] && command -v gtk-update-icon-cache >/dev/null 2>&1 \\\n\t   && [ -f \"$(ICON_THEMEDIR)/index.theme\" ]; then \\\n\t\tgtk-update-icon-cache -q \"$(ICON_THEMEDIR)\"; \\\n\tfi\n"
  },
  {
    "path": "aclocal.m4",
    "content": "# generated automatically by aclocal 1.16.1 -*- Autoconf -*-\n\n# Copyright (C) 1996-2018 Free Software Foundation, Inc.\n\n# This file is free software; the Free Software Foundation\n# gives unlimited permission to copy and/or distribute it,\n# with or without modifications, as long as this notice is preserved.\n\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY, to the extent permitted by law; without\n# even the implied warranty of MERCHANTABILITY or FITNESS FOR A\n# PARTICULAR PURPOSE.\n\nm4_ifndef([AC_CONFIG_MACRO_DIRS], [m4_defun([_AM_CONFIG_MACRO_DIRS], [])m4_defun([AC_CONFIG_MACRO_DIRS], [_AM_CONFIG_MACRO_DIRS($@)])])\ndnl pkg.m4 - Macros to locate and utilise pkg-config.   -*- Autoconf -*-\ndnl serial 11 (pkg-config-0.29)\ndnl\ndnl Copyright © 2004 Scott James Remnant <scott@netsplit.com>.\ndnl Copyright © 2012-2015 Dan Nicholson <dbn.lists@gmail.com>\ndnl\ndnl This program is free software; you can redistribute it and/or modify\ndnl it under the terms of the GNU General Public License as published by\ndnl the Free Software Foundation; either version 2 of the License, or\ndnl (at your option) any later version.\ndnl\ndnl This program is distributed in the hope that it will be useful, but\ndnl WITHOUT ANY WARRANTY; without even the implied warranty of\ndnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\ndnl General Public License for more details.\ndnl\ndnl You should have received a copy of the GNU General Public License\ndnl along with this program; if not, write to the Free Software\ndnl Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA\ndnl 02111-1307, USA.\ndnl\ndnl As a special exception to the GNU General Public License, if you\ndnl distribute this file as part of a program that contains a\ndnl configuration script generated by Autoconf, you may include it under\ndnl the same distribution terms that you use for the rest of that\ndnl program.\n\ndnl PKG_PREREQ(MIN-VERSION)\ndnl -----------------------\ndnl Since: 0.29\ndnl\ndnl Verify that the version of the pkg-config macros are at least\ndnl MIN-VERSION. Unlike PKG_PROG_PKG_CONFIG, which checks the user's\ndnl installed version of pkg-config, this checks the developer's version\ndnl of pkg.m4 when generating configure.\ndnl\ndnl To ensure that this macro is defined, also add:\ndnl m4_ifndef([PKG_PREREQ],\ndnl     [m4_fatal([must install pkg-config 0.29 or later before running autoconf/autogen])])\ndnl\ndnl See the \"Since\" comment for each macro you use to see what version\ndnl of the macros you require.\nm4_defun([PKG_PREREQ],\n[m4_define([PKG_MACROS_VERSION], [0.29])\nm4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1,\n    [m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])])\n])dnl PKG_PREREQ\n\ndnl PKG_PROG_PKG_CONFIG([MIN-VERSION])\ndnl ----------------------------------\ndnl Since: 0.16\ndnl\ndnl Search for the pkg-config tool and set the PKG_CONFIG variable to\ndnl first found in the path. Checks that the version of pkg-config found\ndnl is at least MIN-VERSION. If MIN-VERSION is not specified, 0.9.0 is\ndnl used since that's the first version where most current features of\ndnl pkg-config existed.\nAC_DEFUN([PKG_PROG_PKG_CONFIG],\n[m4_pattern_forbid([^_?PKG_[A-Z_]+$])\nm4_pattern_allow([^PKG_CONFIG(_(PATH|LIBDIR|SYSROOT_DIR|ALLOW_SYSTEM_(CFLAGS|LIBS)))?$])\nm4_pattern_allow([^PKG_CONFIG_(DISABLE_UNINSTALLED|TOP_BUILD_DIR|DEBUG_SPEW)$])\nAC_ARG_VAR([PKG_CONFIG], [path to pkg-config utility])\nAC_ARG_VAR([PKG_CONFIG_PATH], [directories to add to pkg-config's search path])\nAC_ARG_VAR([PKG_CONFIG_LIBDIR], [path overriding pkg-config's built-in search path])\n\nif test \"x$ac_cv_env_PKG_CONFIG_set\" != \"xset\"; then\n\tAC_PATH_TOOL([PKG_CONFIG], [pkg-config])\nfi\nif test -n \"$PKG_CONFIG\"; then\n\t_pkg_min_version=m4_default([$1], [0.9.0])\n\tAC_MSG_CHECKING([pkg-config is at least version $_pkg_min_version])\n\tif $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then\n\t\tAC_MSG_RESULT([yes])\n\telse\n\t\tAC_MSG_RESULT([no])\n\t\tPKG_CONFIG=\"\"\n\tfi\nfi[]dnl\n])dnl PKG_PROG_PKG_CONFIG\n\ndnl PKG_CHECK_EXISTS(MODULES, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])\ndnl -------------------------------------------------------------------\ndnl Since: 0.18\ndnl\ndnl Check to see whether a particular set of modules exists. Similar to\ndnl PKG_CHECK_MODULES(), but does not set variables or print errors.\ndnl\ndnl Please remember that m4 expands AC_REQUIRE([PKG_PROG_PKG_CONFIG])\ndnl only at the first occurence in configure.ac, so if the first place\ndnl it's called might be skipped (such as if it is within an \"if\", you\ndnl have to call PKG_CHECK_EXISTS manually\nAC_DEFUN([PKG_CHECK_EXISTS],\n[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl\nif test -n \"$PKG_CONFIG\" && \\\n    AC_RUN_LOG([$PKG_CONFIG --exists --print-errors \"$1\"]); then\n  m4_default([$2], [:])\nm4_ifvaln([$3], [else\n  $3])dnl\nfi])\n\ndnl _PKG_CONFIG([VARIABLE], [COMMAND], [MODULES])\ndnl ---------------------------------------------\ndnl Internal wrapper calling pkg-config via PKG_CONFIG and setting\ndnl pkg_failed based on the result.\nm4_define([_PKG_CONFIG],\n[if test -n \"$$1\"; then\n    pkg_cv_[]$1=\"$$1\"\n elif test -n \"$PKG_CONFIG\"; then\n    PKG_CHECK_EXISTS([$3],\n                     [pkg_cv_[]$1=`$PKG_CONFIG --[]$2 \"$3\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes ],\n\t\t     [pkg_failed=yes])\n else\n    pkg_failed=untried\nfi[]dnl\n])dnl _PKG_CONFIG\n\ndnl _PKG_SHORT_ERRORS_SUPPORTED\ndnl ---------------------------\ndnl Internal check to see if pkg-config supports short errors.\nAC_DEFUN([_PKG_SHORT_ERRORS_SUPPORTED],\n[AC_REQUIRE([PKG_PROG_PKG_CONFIG])\nif $PKG_CONFIG --atleast-pkgconfig-version 0.20; then\n        _pkg_short_errors_supported=yes\nelse\n        _pkg_short_errors_supported=no\nfi[]dnl\n])dnl _PKG_SHORT_ERRORS_SUPPORTED\n\n\ndnl PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND],\ndnl   [ACTION-IF-NOT-FOUND])\ndnl --------------------------------------------------------------\ndnl Since: 0.4.0\ndnl\ndnl Note that if there is a possibility the first call to\ndnl PKG_CHECK_MODULES might not happen, you should be sure to include an\ndnl explicit call to PKG_PROG_PKG_CONFIG in your configure.ac\nAC_DEFUN([PKG_CHECK_MODULES],\n[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl\nAC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl\nAC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl\n\npkg_failed=no\nAC_MSG_CHECKING([for $1])\n\n_PKG_CONFIG([$1][_CFLAGS], [cflags], [$2])\n_PKG_CONFIG([$1][_LIBS], [libs], [$2])\n\nm4_define([_PKG_TEXT], [Alternatively, you may set the environment variables $1[]_CFLAGS\nand $1[]_LIBS to avoid the need to call pkg-config.\nSee the pkg-config man page for more details.])\n\nif test $pkg_failed = yes; then\n   \tAC_MSG_RESULT([no])\n        _PKG_SHORT_ERRORS_SUPPORTED\n        if test $_pkg_short_errors_supported = yes; then\n\t        $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs \"$2\" 2>&1`\n        else \n\t        $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs \"$2\" 2>&1`\n        fi\n\t# Put the nasty error message in config.log where it belongs\n\techo \"$$1[]_PKG_ERRORS\" >&AS_MESSAGE_LOG_FD\n\n\tm4_default([$4], [AC_MSG_ERROR(\n[Package requirements ($2) were not met:\n\n$$1_PKG_ERRORS\n\nConsider adjusting the PKG_CONFIG_PATH environment variable if you\ninstalled software in a non-standard prefix.\n\n_PKG_TEXT])[]dnl\n        ])\nelif test $pkg_failed = untried; then\n     \tAC_MSG_RESULT([no])\n\tm4_default([$4], [AC_MSG_FAILURE(\n[The pkg-config script could not be found or is too old.  Make sure it\nis in your PATH or set the PKG_CONFIG environment variable to the full\npath to pkg-config.\n\n_PKG_TEXT\n\nTo get pkg-config, see <http://pkg-config.freedesktop.org/>.])[]dnl\n        ])\nelse\n\t$1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS\n\t$1[]_LIBS=$pkg_cv_[]$1[]_LIBS\n        AC_MSG_RESULT([yes])\n\t$3\nfi[]dnl\n])dnl PKG_CHECK_MODULES\n\n\ndnl PKG_CHECK_MODULES_STATIC(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND],\ndnl   [ACTION-IF-NOT-FOUND])\ndnl ---------------------------------------------------------------------\ndnl Since: 0.29\ndnl\ndnl Checks for existence of MODULES and gathers its build flags with\ndnl static libraries enabled. Sets VARIABLE-PREFIX_CFLAGS from --cflags\ndnl and VARIABLE-PREFIX_LIBS from --libs.\ndnl\ndnl Note that if there is a possibility the first call to\ndnl PKG_CHECK_MODULES_STATIC might not happen, you should be sure to\ndnl include an explicit call to PKG_PROG_PKG_CONFIG in your\ndnl configure.ac.\nAC_DEFUN([PKG_CHECK_MODULES_STATIC],\n[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl\n_save_PKG_CONFIG=$PKG_CONFIG\nPKG_CONFIG=\"$PKG_CONFIG --static\"\nPKG_CHECK_MODULES($@)\nPKG_CONFIG=$_save_PKG_CONFIG[]dnl\n])dnl PKG_CHECK_MODULES_STATIC\n\n\ndnl PKG_INSTALLDIR([DIRECTORY])\ndnl -------------------------\ndnl Since: 0.27\ndnl\ndnl Substitutes the variable pkgconfigdir as the location where a module\ndnl should install pkg-config .pc files. By default the directory is\ndnl $libdir/pkgconfig, but the default can be changed by passing\ndnl DIRECTORY. The user can override through the --with-pkgconfigdir\ndnl parameter.\nAC_DEFUN([PKG_INSTALLDIR],\n[m4_pushdef([pkg_default], [m4_default([$1], ['${libdir}/pkgconfig'])])\nm4_pushdef([pkg_description],\n    [pkg-config installation directory @<:@]pkg_default[@:>@])\nAC_ARG_WITH([pkgconfigdir],\n    [AS_HELP_STRING([--with-pkgconfigdir], pkg_description)],,\n    [with_pkgconfigdir=]pkg_default)\nAC_SUBST([pkgconfigdir], [$with_pkgconfigdir])\nm4_popdef([pkg_default])\nm4_popdef([pkg_description])\n])dnl PKG_INSTALLDIR\n\n\ndnl PKG_NOARCH_INSTALLDIR([DIRECTORY])\ndnl --------------------------------\ndnl Since: 0.27\ndnl\ndnl Substitutes the variable noarch_pkgconfigdir as the location where a\ndnl module should install arch-independent pkg-config .pc files. By\ndnl default the directory is $datadir/pkgconfig, but the default can be\ndnl changed by passing DIRECTORY. The user can override through the\ndnl --with-noarch-pkgconfigdir parameter.\nAC_DEFUN([PKG_NOARCH_INSTALLDIR],\n[m4_pushdef([pkg_default], [m4_default([$1], ['${datadir}/pkgconfig'])])\nm4_pushdef([pkg_description],\n    [pkg-config arch-independent installation directory @<:@]pkg_default[@:>@])\nAC_ARG_WITH([noarch-pkgconfigdir],\n    [AS_HELP_STRING([--with-noarch-pkgconfigdir], pkg_description)],,\n    [with_noarch_pkgconfigdir=]pkg_default)\nAC_SUBST([noarch_pkgconfigdir], [$with_noarch_pkgconfigdir])\nm4_popdef([pkg_default])\nm4_popdef([pkg_description])\n])dnl PKG_NOARCH_INSTALLDIR\n\n\ndnl PKG_CHECK_VAR(VARIABLE, MODULE, CONFIG-VARIABLE,\ndnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])\ndnl -------------------------------------------\ndnl Since: 0.28\ndnl\ndnl Retrieves the value of the pkg-config variable for the given module.\nAC_DEFUN([PKG_CHECK_VAR],\n[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl\nAC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config])dnl\n\n_PKG_CONFIG([$1], [variable=\"][$3][\"], [$2])\nAS_VAR_COPY([$1], [pkg_cv_][$1])\n\nAS_VAR_IF([$1], [\"\"], [$5], [$4])dnl\n])dnl PKG_CHECK_VAR\n\n"
  },
  {
    "path": "changelog.md",
    "content": "# Changelog\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## 2.5.10 - 2026-01-30\n\n### Added\n*   Implement Feature Request: Add configuration option 'disable_version_check' (#3530)\n*   Implement Feature Request: Add automatic debug logging output redaction (#3549)\n\n### Changed\n*   Improve --resync warning prompt for clarity and safer operation (#3562)\n*   Updated Dockerfiles to support newer distributions and associated components (#3565)\n*   FreeBSD: Select inotify type (libc or libnotify) based on FreeBSD version (#3579)\n*   Update that --force and --force-sync cannot be used with --resync (#3593)\n\n### Fixed\n*   Fix Bug: Fix timestamp and hash evaluation to avoid unnecessary file version creation online (#3526)\n*   Fix Bug: Fix that websocket do not work with Sharepoint libraries (#3533)\n*   Fix Bug: Fix that large files fail to download due operational timeout being exceeded (#3541)\n*   Fix Bug: Fix hash functions read efficiency to support 'on-demand' development work (#3544)\n*   Fix Bug: Fix that safeBackup crashes when attempting backing up a non-existent local path (#3545)\n*   Fix Bug: Fix to that the application only performs safeBackup() on deleted items only when a hash change is detected (#3546)\n*   Fix Bug: Prevent mis-configuration where 'recycle_bin_path' is inside 'sync_dir' (#3552)\n*   Fix Bug: Harden logging initialisation: fall back to home directory when log_dir is not writeable (#3555)\n*   Fix Bug: Ensure mkdirRecurse() is correctly wrapped in try block (#3566)\n*   Fix Bug: Fix that 'remove_source_files' does not remove the source file when the file already exists in OneDrive (#3572)\n*   Fix Bug: Enhance displayFileSystemErrorMessage() to include details of the actual path (#3574)\n*   Fix Bug: Enhance downloadFileItem() to ensure greater clarity on download failures (#3575)\n*   Fix Bug: Prevent malformed 'skip_dir' / 'skip_file' rules when using multiple config entries (#3576)\n*   Fix Bug: Detect and prevent 'skip_dir' / 'skip_file' rules shadowing 'sync_list' inclusions (#3577)\n*   Fix Bug: Fix 'skip_dir' and 'skip_file' shadow detection for rooted 'sync_list' paths (#3578)\n*   Fix Bug: Fix 'skip_dir' directory exclusion by normalising input paths before matching (#3580)\n*   Fix Bug: Fix testInternetReachability() function to ensure same curl options used in a consistent manner (#3581)\n*   Fix Bug: Fix performPermanentDelete() to ensure zero content length is set (#3585)\n*   Fix Bug: Fix safeRemove() to retry on EINTR / EBUSY filesystem responses to support 'on-demand' development work (#3586)\n*   Fix Bug: Fix safeRename() to retry on EINTR / EBUSY filesystem responses to support 'on-demand' development work (#3587)\n*   Fix Bug: Fix safeBackup() to retry on EINTR / EBUSY filesystem responses to support 'on-demand' development work (#3589)\n*   Fix Bug: Fix WebSocket reconnect cleanup to prevent GC finalisation crash (#3582)\n*   Fix Bug: Fix setLocalPathTimestamp() to retry on EINTR / EBUSY filesystem responses to support 'on-demand' development work (#3591)\n*   Fix Bug: Fix incorrect handling of failed safeRename() operations to support 'on-demand' development work (#3592) \n*   Fix Bug: Fix Docker entrypoint handling for non-root --user execution (#3602)\n*   Fix Bug: Fix getRemainingFreeSpaceOnline() and correctly handle zero data traversal events for quota tracking (#3618)\n*   Fix Bug: Fix getRemainingFreeSpaceOnline() for Business and SharePoint Accounts (#3621)\n*   Fix Bug: Fix OAuth authorisation code parsing and encoding during token redemption (#3625)\n*   Fix Bug: Fix Graph search(q=…) escaping for apostrophes (#3624)\n*   Fix Bug: Fix handling of 204 No Content responses for Microsoft Graph PATCH requests (#3620)\n\n### Updated\n*   Updated completion files to align to application functionality\n*   Updated documentation\n\n\n## 2.5.9 - 2025-11-06\n\n### Fixed\n*   Fix Bug: Fix very high CPU & memory utilisation with 2.5.8 when using --upload-only (#3515) (CRITICAL BUGFIX)\n*   Fix Bug: Unexpected deletion of empty nested folders during first sync with 'sync_list' and --resync (#3513) (CRITICAL BUGFIX)\n\n### Updated\n*   Updated documentation\n\n\n## 2.5.8 - 2025-11-05\n\n### Added\n*   Implement Feature Request: Add that dotfiles in sync_list should be synced even when skip_dotfiles = \"true\" (#3456)\n*   Implement Feature Request: Add websocket notification support (#3413)\n*   Implement Feature Request: Add --download-file feature (#3459)\n*   Implement Feature Request: Add option to remove source folders when using --upload-only --remove-source-files (#3473)\n*   Implement Feature Request: Add support for AlmaLinux (#3485)\n*   Implement Feature Request: Add ONEDRIVE_THREADS Docker option (#3494)\n*   Implement Feature Request: Implement Desktop Manager Integration for GNOME and KDE (#3500)\n\n### Changed\n*   Changed how the file path is computed when there are 'skip_dir' entries to be consistent (#3484)\n*   Changed checkJSONAgainstClientSideFiltering() to avoid multiple calls to computeItemPath() (#3489)\n\n### Fixed\n*   Fix Bug: Ensure driveId target is cached for modified file uploads (#3454)\n*   Fix Bug: Ensure that 'use_intune_sso' and 'use_device_auth' cannot be used together (#3453)\n*   Fix Bug: Force DNS Timeout when forcing a libcurl fresh connection (#3468)\n*   Fix Bug: Fix WebSocket connection failure on libcurl 8.12.x by forcing HTTP/1.1 and disabling ALPN/NPN (#3482)\n*   Fix Bug: Fix application crash after deleting file locally (#3481)\n*   Fix Bug: Fix missing user information when syncing shared files (#3483)\n*   Fix Bug: Fix Shared Folder data being deleted due to 'skip_dir' entry of '.*' (#3476)\n*   Fix Bug: Fix that if using 'sync_list' only add new JSON items early to allow applyPotentiallyChangedItem() to operate as expected (#3505)\n*   Fix Bug: When using --dry-run use tracked renamed directories to avoid falsely indicating local data is new and uploading as new data (#3503)\n*   Fix Bug: Fix the fetching of maximum open files to be more POSIX compliant (#3508)\n*   Fix Bug: Fix the Handling of WebSocket 'echo' from a local change (#3509)\n\n### Updated\n*   Updated documentation\n\n\n## 2.5.7 - 2025-09-23\n\n### Added\n*   Implement Feature Request: Show GUI notification when sync suspends due to 'large delete' threshold (#3388)\n*   Implement Feature Request: Implement resumable downloads (#3354)\n\n### Changed\n*   Removed the auto configuration of using a larger fragment size (#3370)\n*   Removed the OpenSSL Test (#3420)\n\n### Fixed\n*   Fix Bug: Catch unhandled OneDriveError exception due to libcurl failing to access the system CA certificate bundle (#3322)\n*   Fix Bug: 'items-dryrun.sqlite3' gets erroneously created when running a 'no sync' operation (#3325)\n*   Fix Bug: Handle online folder deletion|creation with same name that causes 'id' to change (#3332)\n*   Fix Bug: Reduce I/O pressure on SQLite DB Operations (#3334)\n*   Fix Bug: Handle a 409 online folder creation response with a re-query of the API (#3335)\n*   Fix Bug: Fix systemd issue with ExecStartPre statement to be more OS independent (#3348)\n*   Fix Bug: When using --upload-only do not try and update the local file timestamp post upload (#3349)\n*   Fix Bug: Add missing 'config' options to --display-config (#3353)\n*   Fix Bug: Fix that a failed file download can lead to online deletion (#3351)\n*   Fix Bug: Update searchDriveItemForFile() to handle specific 404 response when file cannot be found (#3365)\n*   Fix Bug: Fix that resync state remains true post first successful full sync (#3368)\n*   Fix Bug: Fix that long running big upload (250GB+) fails because of an expired access token (#3361)\n*   Fix Bug: Handle inconsistent OneDrive Personal driveId casing across multiple Microsoft Graph API Endpoints (#3347)\n*   Fix Bug: Update Microsoft OneNote handling for 'OneNote_RecycleBin' objects (#3350)\n*   Fix Bug: Handle invalid JSON response when querying parental details (#3379)\n*   Fix Bug: Fix foreign key issue when performing a --resync due to a missed conversion of driveId to lowercase values and path is covered by 'sync_list' entries (#3383)\n*   Fix Bug: Ensure 'sync_list' inclusion rules are correctly evaluated (#3381)\n*   Fix Bug: Fix issue of trying to create the root folder online (#3403)\n*   Fix Bug: Fix resumable downloads so that the curl engine offset point is reset post successful download (#3406)\n*   Fix Bug: Fix application crash when a file is created and deleted quickly (#3405)\n*   Fix Bug: Fix the support of relocated shared folders for OneDrive Personal (#3411)\n*   Fix Bug: Fix infinite loop after a failed network connection due to changed curl messaging (#3412)\n*   Fix Bug: Fix computePath() to track the parental path anchor when a Shared Folder is relocated with a deeper path (#3417)\n*   Fix Bug: Fix SharePoint Shared Library DB Tie creation (#3419)\n*   Fix Bug: Update safeBackup() function to ensure that the 'safeBackup' path addition is only added once and ignore directories (#3445)\n\n### Updated\n*   Updated OAuth2 Interactive Authorisation Flow prompts to remove any ambiguity on what actions a user needs to take (#3323)\n*   Updated onedrive.spec.in to correct missing dependencies (#3329)\n*   Updated minimum compiler version details (#3330)\n*   Updated documentation and function for how 'threads' is used (#3352)\n*   Updated logging output for upsert() function (#3333)\n*   Updated curl 8.13.x and 8.14.0 to known bad curl versions (#3356)\n*   Updated logging output when processing online deletion events (#3373)\n*   Updated logging output and use of grandparent identifiers when using --dry-run (#3377)\n*   Updated GitHub Action versions for building Docker containers (#3378)\n*   Updated how the ETA values are calculated to avoid negative values (#3386)\n*   Updated Debian Dockerfile to use upstream gosu (#3402)\n*   Updated Debian Dockerfile to use 'bookworm' (#3402)\n*   Updated documentation\n\n\n## 2.5.6 - 2025-06-05\n\n### Added\n*   Enhancement: Add gdc support to enable Gentoo compilation\n*   Enhancement: Add a notification to user regarding number of objects received from OneDrive API\n*   Enhancement: Update 'skip_file' documentation and option validation\n*   Enhancement: Add a new configuration option 'force_session_upload' to support editors and applications using atomic save operations\n*   Enhancement: Added 2 functions to check for the presence of required remoteItem elements to create a Shared Folder DB entries\n*   Implement Feature Request: Add local recycle bin or trash folder option\n*   Implement Feature Request: Add configurable upload delay to support Obsidian\n*   Implement Feature Request: Add validation of bools in config file \n*   Implement Feature Request: Add native support for authentication via Intune dbus interface \n*   Implement Feature Request: Implement OAuth2 Device Authorisation Flow \n\n### Changed\n*   Change logging output level for JSON elements that contain URL encoding\n*   Change 'configure.ac' to use a static date value as Debian 'reproducible' build process forces a future date to rebuild any code to determine reproducibility\n\n### Fixed\n*   Fix Regression: Fixed regression in handling Microsoft OneNote package folders being created in error \n*   Fix Regression: Fix OneNote file MimeType detection\n*   Fix Regression: Fix supporting Personal Shared Folders that have been renamed\n*   Fix Bug: Correct the logging output for 'skip_file' exclusions \n*   Fix Bug: Validate raw JSON from Graph API for 15 character driveId API bug\n*   Fix Bug: Fix JSON exception on webhook subscription renewal due to 308 redirect\n*   Fix Bug: Update 'sync_list' line parsing to correctly escape characters for regex parsing\n*   Fix Bug: Fix that an empty folder or folder with Microsoft OneNote files are deleted online when content is shared from a SharePoint Library Document Root\n*   Fix Bug: Fix that empty 'skip_file' forces resync indefinitely\n*   Fix Bug: Fix that 'sync_list' rule segment|depth check fails in some scenarios and implement a better applicable mechanism check\n*   Fix Bug: Resolve crash when getpwuid() breaks when there is a glibc version mismatch\n*   Fix Bug: Resolve crash when opening file fails when computing file hash\n*   Fix Bug: Add check for invalid exclusion 'sync_list' exclusion rules\n*   Fix Bug: Fix uploading of modified files when using --upload-only & --remove-source-files\n*   Fix Bug: Fix local path calculation for Relocated OneDrive Business Shared Folders \n*   Fix Bug: Fix 'sync_list' anywhere rule online directory creation\n*   Fix Bug: Fix online path creation to ensure parental path structure is created in a consistent manner\n*   Fix Bug: Fix handling of POSIX check for existing online items\n*   Fix Bug: Fix args printing in dockerfile entrypoint\n*   Fix Bug: Fix the testing of parental structure for 'sync_list' inclusion when adding inotify watches\n*   Fix Bug: Fix failure to handle API 403 response when file fragment upload fails\n*   Fix Bug: Fix application notification output to be consistent when skipping integrity checks\n*   Fix Bug: Fix how local timestamps are modified \n*   Fix Bug: Fix how online remaining free space is calculated and consumed internally for free space tracking\n*   Fix Bug: Fix logic of determining if a file has valid integrity when using --disable-upload-validation\n*   Fix Bug: Format the OneDrive change into a consumable object for the database earlier to use values in application logging\n*   Fix Bug: Fix upload session offset handling to prevent desynchronisation on large files\n*   Fix Bug: Fix implementation of 'write_xattr_data' to support FreeBSD\n*   Fix Bug: Update hash functions to ensure file is closed if opened \n*   Fix Bug: Dont blindly run safeBackup() if the online timestamp is newer \n*   Fix Bug: Only set xattr values when not using --dry-run \n*   Fix Bug: Fix UTC conversion for existing file timestamp post file download\n*   Fix Bug: Fix that 'check_nosync' and 'skip_size' configuration options when changed, were not triggering a --resync correctly\n*   Fix Bug: Ensure file is closed before renaming to improve compatibility with GCS buckets and network filesystems \n*   Fix Bug: If a file fails to download, path fails to exist. Check path existence before setting xattr values\n\n### Updated\n*   Updated .gitignore to ignore files created during configure to be consistent with other files generated from .in templates\n*   Updated bash,fish and zsh completion files to align with application options\n*   Updated 'config' file to align to application options with applicable descriptions\n*   Updated testbuild runner\n*   Updated Fedora Docker OS version to Fedora 42\n*   Updated Ubuntu 24.10 curl version 8.9.1 to known bad curl versions and document the bugs associated with it\n*   Updated Makefile to pass libraries after source files in compiler invocation\n*   Updated 'configure.ac' to support more basename formats for DC\n*   Update how threads are set based on available CPUs \n*   Update setLocalPathTimestamp logging output \n*   Update when to perform thread check and set as early as possible \n*   Updated documentation\n\n## 2.5.5 - 2025-03-17\n\n### Added\n*   Implement Feature Request: Implement 'transfer_order' configuration option to allow the user to determine what order files are transferred in\n*   Implement Feature Request: Implement 'disable_permission_set' configuration option to not set directory and file permissions\n*   Implement Feature Request: Implement 'write_xattr_data' configuration option to add information about file creator/last editor as extended file attributes\n*   Enhancement: Add support for --share-password option when --create-share-link is called\n*   Enhancement: Add support 'localizedMessage' error messages in application output if this is provided in the JSON response from Microsoft Graph API\n\n### Changed\n*   Changed curl debug logging to --debug-https as this is more relevant\n*   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\n*   Comprehensively changed OneDrive Personal 'driveId' value checking due to major OneDrive API backend platform user migration and major differences in API response output\n\n### Fixed\n*   Fix Bug: Fix path calculation for Client Side Filtering evaluations for Personal Accounts\n*   Fix Bug: Fix path calculation for Client Side Filtering evaluations for Business Accounts\n*   Fix Bug: Only perform path calculation if this is actually required\n*   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\n*   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\n*   Fix Bug: When removing a OneDrive Personal Shared Folder, remove the actual link, not the remote user folder\n*   Fix Bug: Fix 'Unsupported platform' for inotify watches by using the correct predefined version definition for Linux.\n\n### Updated\n*   Updated Fedora Docker OS version to Fedora 41\n*   Updated Alpine Docker OS version to Alpine 3.21\n*   Updated documentation\n\n## 2.5.4 - 2025-02-03\n\n### Added\n*   Implement Feature Request: Support Permanent Delete on OneDrive\n*   Implement Feature Request: Support the moving of Shared Folder Links to other folders (Business Accounts only)\n*   Enhancement: Added due to ongoing Ubuntu issues with 'curl' and 'libcurl', updated the documentation to include all relevant curl bugs and affected versions\n*   Enhancement: Added quota status messages for nearing | critical | exceeded based on OneDrive Account API response\n*   Enhancement: Added Docker variable to implement a sync once option\n*   Enhancement: Added configuration option 'create_new_file_version' to force create new versions if that is the desire\n*   Enhancement: Added support for adding SharePoint Libraries as Shared Folder Links\n*   Enhancement: Added code and documentation changes to support FreeBSD\n*   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\n*   Enhancement: Added './' sync_list rule check as this does not align to the documentation and these rules will not get matched correctly.\n\n### Changed\n*   Changed how debug logging outputs HTTP response headers and when this occurs\n*   Changed when the check for no --sync | --monitor occurs so that this fails faster to avoid setting up all the other components\n*   Changed isValidUTF8 function to use 'validate' rather than individual character checking and enhance checks including length constraints\n*   Changed --dry-run authentication message to remove ambiguity that --dry-run cannot be used to authenticate the application\n\n### Fixed\n*   Fix Regression: Fixed regression that sync_list does not traverse shared directories\n*   Fix Regression: Fixed regression of --display-config use after fast failing if --sync or --monitor has not been used\n*   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\n*   Fix Regression: Add back file transfer metrics which was available in v2.4.x\n*   Fix Regression: Add code to support using 'display_processing_time' for functional performance which was available in v2.4.x\n*   Fix Bug: Fixed build issue for OpenBSD (however support for OpenBSD itself is still a work-in-progress)\n*   Fix Bug: Fixed issue regarding parsing OpenSSL and when unable to be parsed, do not force the application to exit\n*   Fix Bug: Fixed the import of 'sync_list' rules due to OneDriveGUI creating a blank empty file by default\n*   Fix Bug: Fixed the display of 'sync_list' rules due to OneDriveGUI creating a blank empty file by default\n*   Fix Bug: Fixed that Business Shared Items shortcuts are skipped as being incorrectly detected as Microsoft OneNote Notebook items\n*   Fix Bug: Fixed space calculations due to using ulong variable type to ensure that if calculation is negative, value is negative\n*   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\n*   Fix Bug: Fixed skip_dir logic when reverse traversing folder structure\n*   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\n*   Fix Bug: Fixed path got deleted in handling of move & close_write event when using 'vim'.\n*   Fix Bug: Fixed that the root Personal Shared Folder is not handled due to missing API data European Data Centres\n*   Fix Bug: Fixed the the local timestamp is not set when using --disable-download-validation\n*   Fix Bug: Fixed Upload|Download Loop for AIP Protected File in Monitor Mode\n*   Fix Bug: Fixed --single-directory Shared Folder DB entry creation\n*   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\n*   Fix Bug: Fixed testInternetReachability function so that this always returns a boolean value and not throw an exception\n\n### Updated\n*   Updated documentation\n\n## 2.5.3 - 2024-11-16\n\n### Added\n*   Implement Feature Request: Implement Docker ENV variable for --cleanup-local-files\n*   Enhancement: Setup a specific SIGPIPE Signal handler for curl/openssl generated signals\n*   Enhancement: Add Check Spelling GitHub Action\n*   Enhancement: Add passive database checkpoints to optimise database operations\n*   Enhancement: Ensure application notifies user of curl versions that contain HTTP/2 bugs that impact the operation of this client\n*   Enhancement: Add OpenSSL version warning\n*   Enhancement: Improve performance with reduced execution time and lower CPU/system resource usage\n\n### Changed\n*   Specifically use a 'mutex' to perform the lock on database actions\n*   Update safeBackup to use a new filename format for easier identification: filename-hostname-safeBackup-number.file_extension\n*   Allow no-sync operations to complete online account checks\n\n### Fixed\n*   Fix Regression: Fix regression for Docker 'sync_dir' use\n*   Fix Bug: Fix that a 'sync_list' entry of '/' will cause a index [0] is out of bounds\n*   Fix Bug: Fix that when creating a new folder online the application generates an exception if it is in a Shared Online Folder\n*   Fix Bug: Fix application crash when session upload files contain zero data or are corrupt\n*   Fix Bug: Fix that curl generates a SIGPIPE that causes application to exit due to upstream device killing idle TCP connection\n*   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\n*   Fix Bug: Fix application crash caused by unable to drop table\n*   Fix Bug: Fix that skip_file in config does not override defaults\n*   Fix Bug: Handle DB upgrades from v2.4.x without causing application crash\n*   Fix Bug: Fix a database statement execution error occurred: NOT NULL constraint failed: item.type due to Microsoft OneNote items\n*   Fix Bug: Fix Operation not permitted FileException Error when attempting to use setTimes() function\n*   Fix Bug: Fix that files with no mime type cause sync to crash\n*   Fix Bug: Fix that bypass_data_preservation operates as intended\n\n### Updated\n*   Fixed spelling errors across all documentation and code\n*   Update Dockerfile-debian to fix that libcurl4 does not get applied despite being pulled in. Explicitly install it from Debian 12 Backports\n*   Add Ubuntu 24.10 OpenSuSE Build Service details\n*   Update Dockerfile-alpine - revert to Alpine 3.19 as application fails to run on Alpine 3.20\n*   Updated documentation\n\n## 2.5.2 - 2024-09-29\n\n### Added\n*   Added 15 second sleep to systemd services to allow d-bus daemon to start and be available if present\n\n### Fixed\n*   Fix Bug: Application crash unable to correctly process a timestamp that has fractional seconds\n*   Fix Bug: Fixed application logging output of Personal Shared Folder incorrectly advising there is no free space\n\n### Updated\n*   Updated documentation\n\n## 2.5.1 - 2024-09-27 (DO NOT USE. CONTAINS A MAJOR TIMESTAMP ISSUE BUG)\n\n### Special Thankyou\nA special thankyou to @phlibi for assistance with diagnosing and troubleshooting the database timestamp issue\n\n### Added\n*   Implement Feature Request: Don't print the d-bus WARNING if disable_notifications is set on cmd line or in config\n\n### Changed\n*   Add --enable-debug to Docker files when building client application to allow for better diagnostics when issues occur\n*   Update Debian Dockerfile to use 'curl' from backports so a more modern curl version is used\n\n### Fixed\n*   Fix Regression: Fix regression of extra quotation marks when using ONEDRIVE_SINGLE_DIRECTORY with Docker\n*   Fix Regression: Fix regression that real-time synchronization is not occurring when using --monitor and sync_list\n*   Fix Regression: Fix regression that --remove-source-files doesn’t work\n*   Fix Bug: Application crash when run synchronize due to negative free space online\n*   Fix Bug: Application crash when performing a URL decode\n*   Fix Bug: Application crash when using sync_list and Personal Shared Folders the root folder fails to present the item id\n*   Fix Bug: Application crash when attempting to read timestamp from database as invalid data was written\n\n### Updated\n*   Updated documentation (various)\n\n## 2.5.0 - 2024-09-16\n\n### Special Thankyou\nA special thankyou to all those who helped with testing and providing feedback during the development of this major release. A big thankyou to:\n*   @JC-comp\n*   @Lyncredible\n*   @rrodrigueznt\n*   @bpozdena\n*   @hskrieg\n*   @robertschulze \n*   @aothmane-control\n*   @mozram\n*   @LunCh-CECNL\n*   @pkolmann\n*   @tdcockers\n*   @undefiened\n*   @cyb3rko\n\n### Notable Changes\n*   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.\n*   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.\n*   The application function --download-only no longer automatically deletes local files. Please read the new documentation regarding this feature.\n\n### Added\n*   Implement Feature Request: Multi-threaded uploading/downloading of files\n*   Implement Feature Request: Renaming/Relocation of OneDrive Business shared folders\n*   Implement Feature Request: Support the syncing of individual business shared files\n*   Implement Feature Request: Implement application output to detail upload|download failures at the end of a sync process\n*   Implement Feature Request: Log when manual Authorization is required when using --auth-files\n*   Implement Feature Request: Add cmdline parameter to display (human readable) quota status\n*   Implement Feature Request: Add capability to disable 'fullscan_frequency'\n*   Implement Feature Request: Ability to set --disable-download-validation from Docker environment variable\n*   Implement Feature Request: Ability to set --sync-shared-files from Docker environment variable\n*   Implement Feature Request: file sync (upload/download/delete) notifications\n\n### Changed\n*   Renamed various documentation files to align with document content\n*   Implement buffered logging so that all logging from all upload & download activities are handled correctly\n*   Replace polling monitor loop with blocking wait\n*   Update how the application utilises curl to fix socket reuse\n*   Various performance enhancements\n*   Implement refactored OneDrive API logic\n*   Enforcement of operational conflicts\n*   Enforcement of application configuration defaults and minimums\n*   Utilise threadsafe sqlite DB access methods\n*   Various bugs and other issues identified during development and testing\n*   Various code cleanup and optimisations\n\n### Fixed\n*   Fix Bug: Upload only not working with Business shared folders\n*   Fix Bug: Business shared folders with same basename get merged\n*   Fix Bug: --dry-run prevents authorization\n*   Fix Bug: Log timestamps lacking trailing zeros, leading to poor log file output alignment\n*   Fix Bug: Subscription ID already exists when using webhooks\n*   Fix Bug: Not all files being downloaded when API data includes HTML ASCII Control Sequences\n*   Fix Bug: --display-sync-status does not work when OneNote sections (.one files) are in your OneDrive\n*   Fix Bug: vim backups when editing files cause edited file to be deleted rather than the edited file being uploaded\n*   Fix Bug: skip_dir does not always work as intended for all directory entries\n*   Fix Bug: Online date being changed in download-only mode\n*   Fix Bug: Resolve that download_only = \"true\" and cleanup_local_files = \"true\" also deletes files present online\n*   Fix Bug: Resolve that upload session are not canceled with resync option\n*   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\n*   Fix Bug: Files with newer timestamp are not chosen as version to be kept\n*   Fix Bug: Synced file is removed when updated on the remote while being processed by onedrive\n*   Fix Bug: Cannot select/filter within Personal Shared Folders\n*   Fix Bug: HTML encoding requires to add filter entries twice\n*   Fix Bug: Uploading files using fragments stuck at 0%\n*   Fix Bug: Implement safeguard when sync_dir is missing and is re-created data is not deleted online\n*   Fix Bug: Fix that --get-sharepoint-drive-id does not handle a SharePoint site with more than 200 entries\n*   Fix Bug: Fix that 'sync_list' does not include files that should be included, when specified just as *.ext_type\n*   Fix Bug: Fix 'sync_list' processing so that '.folder_name' is excluded but 'folder_name' is included\n\n### Updated\n*   Overhauled all documentation\n\n## 2.4.25 - 2023-06-21\n\n### Fixed\n*   Fixed that the application was reporting as v2.2.24 when in fact it was v2.4.24 (release tagging issue)\n*   Fixed that the running version obsolete flag (due to above issue) was causing a false flag as being obsolete\n*   Fixed that zero-byte files do not have a hash as reported by the OneDrive API thus should not generate an error message\n\n### Updated\n*   Update to Debian Docker file to resolve Docker image Operating System reported vulnerabilities\n*   Update to Alpine Docker file to resolve Docker image Operating System reported vulnerabilities\n*   Update to Fedora Docker file to resolve Docker image Operating System reported vulnerabilities\n*   Updated documentation (various)\n\n## 2.4.24 - 2023-06-20\n### Fixed\n*   Fix for extra encoded quotation marks surrounding Docker environment variables\n*   Fix webhook subscription creation for SharePoint Libraries\n*   Fix that a HTTP 504 - Gateway Timeout causes local files to be deleted when using --download-only & --cleanup-local-files mode\n*   Fix that folders are renamed despite using --dry-run\n*   Fix deprecation warnings with dmd 2.103.0\n*   Fix error that the application is unable to perform a database vacuum: out of memory when exiting\n\n### Removed\n*   Remove sha1 from being used by the client as this is being deprecated by Microsoft in July 2023\n*   Complete the removal of crc32 elements\n\n### Added\n*   Added ONEDRIVE_SINGLE_DIRECTORY configuration capability to Docker\n*   Added --get-file-link shell completion\n*   Added configuration to allow HTTP session timeout(s) tuning via config (taken from v2.5.x)\n\n### Updated\n*   Update to Debian Docker file to resolve Docker image Operating System reported vulnerabilities\n*   Update to Alpine Docker file to resolve Docker image Operating System reported vulnerabilities\n*   Update to Fedora Docker file to resolve Docker image Operating System reported vulnerabilities\n*   Updated cgi.d to commit 680003a - last upstream change before requiring `core.d` dependency requirement\n*   Updated documentation (various)\n\n## 2.4.23 - 2023-01-06\n### Fixed\n*   Fixed RHEL7, RHEL8 and RHEL9 Makefile and SPEC file compatibility\n\n### Removed\n*   Disable systemd 'PrivateUsers' due to issues with systemd running processes when option is enabled, causes local file deletes on RHEL based systems\n\n### Updated\n*   Update --get-O365-drive-id error handling to display a more a more appropriate error message if the API cannot be found\n*   Update the GitHub version check to utilise the date a release was done, to allow 1 month grace period before generating obsolete version message\n*   Update Alpine Dockerfile to use Alpine 3.17 and Golang 1.19\n*   Update handling of --source-directory and --destination-directory if one is empty or missing and if used with --synchronize or --monitor\n*   Updated documentation (various)\n\n## 2.4.22 - 2022-12-06\n### Fixed\n*   Fix application crash when local file is changed to a symbolic link with non-existent target\n*   Fix build error with dmd-2.101.0\n*   Fix build error with LDC 1.28.1 on Alpine\n*   Fix issue of silent exit when unable to delete local files when using --cleanup-local-files\n*   Fix application crash due to access permissions on configured path for sync_dir\n*   Fix potential application crash when exiting due to failure state and unable to cleanly shutdown the database\n*   Fix creation of parent empty directories when parent is excluded by sync_list\n\n### Added\n*   Added performance output details for key functions\n\n### Changed\n*   Switch Docker 'latest' to point at Debian builds rather than Fedora due to ongoing Fedora build failures\n*   Align application logging events to actual application defaults for --monitor operations\n*   Performance Improvement: Avoid duplicate costly path calculations and DB operations if not required\n*   Disable non-working remaining sandboxing options within systemd service files\n*   Performance Improvement: Only check 'sync_list' if this has been enabled and configured\n*   Display 'Sync with OneDrive is complete' when using --synchronize\n*   Change the order of processing between Microsoft OneDrive restrictions and limitations check and skip_file|skip_dir check\n\n### Removed\n*   Remove building Fedora ARMv7 builds due to ongoing build failures\n\n### Updated\n*   Update config change detection handling\n*   Updated documentation (various)\n\n## 2.4.21 - 2022-09-27\n### Fixed\n*   Fix that the download progress bar doesn't always reach 100% when rate_limit is set\n*   Fix --resync handling of database file removal\n*   Fix Makefile to be consistent with permissions that are being used\n*   Fix that logging output for skipped uploaded files is missing\n*   Fix to allow non-sync tasks while sync is running\n*   Fix where --resync is enforced for non-sync operations\n*   Fix to resolve segfault when running 'onedrive --display-sync-status' when run as 2nd process\n*   Fix DMD 2.100.2 depreciation warning\n\n### Added\n*   Add GitHub Action Test Build Workflow (replacing Travis CI)\n*   Add option --display-running-config to display the running configuration as used at application startup\n*   Add 'config' option to request readonly access in oauth authorization step\n*   Add option --cleanup-local-files to cleanup local files regardless of sync state when using --download-only\n*   Add option --with-editing-perms to create a read-write shareable link when used with --create-share-link <file>\n\n### Changed\n*   Change the exit code of the application to 126 when a --resync is required\n\n### Updated\n*   Updated --get-O365-drive-id implementation for data access\n*   Update what application options require an argument\n*   Update application logging output for error messages to remove certain \\n prefix when logging to a file\n*   Update onedrive.spec.in to fix error building RPM\n*   Update GUI notification handling for specific skipped scenarios\n*   Updated documentation (various)\n\n## 2.4.20 - 2022-07-20\n### Fixed\n*   Fix 'foreign key constraint failed' when using OneDrive Business Shared Folders due to change to using /delta query\n*   Fix various little spelling errors (checked with lintian during Debian packaging)\n*   Fix handling of a custom configuration directory when using --confdir\n*   Fix to ensure that any active http instance is shutdown before any application exit\n*   Fix to enforce that --confdir must be a directory\n\n### Added\n*   Added 'force_http_11' configuration option to allow forcing HTTP/1.1 operations\n\n### Changed\n*   Increased thread sleep for better process I/O wait handling\n*   Removed 'force_http_2' configuration option\n\n### Updated\n*   Update OneDrive API response handling for National Cloud Deployments\n*   Updated to switch to using curl defaults for HTTP/2 operations\n*   Updated documentation (various)\n\n## 2.4.19 - 2022-06-15\n### Fixed\n*   Update Business Shared Folders to use a /delta query\n*   Update when DB is updated by OneDrive API data and update when file hash is required to be generated\n\n### Added\n*   Added ONEDRIVE_UPLOADONLY flag for Docker\n\n### Updated\n*   Updated GitHub workflows\n*   Updated documentation (various)\n\n## 2.4.18 - 2022-06-02\n### Fixed\n*   Fixed various database related access issues stemming from running multiple instances of the application at the same time using the same configuration data\n*   Fixed --display-config being impacted by --resync flag\n*   Fixed installation permissions for onedrive man-pages file\n*   Fixed that in some situations that users try --upload-only and --download-only together which is not possible\n*   Fixed application crash if unable to read required hash files\n\n### Added\n*   Added Feature Request to add an override for skip_dir|skip_file through flag to force sync\n*   Added a check to validate local filesystem available space before attempting file download\n*   Added GitHub Actions to build Docker containers and push to DockerHub \n\n### Updated\n*   Updated all Docker build files to current distributions, using updated distribution LDC version\n*   Updated logging output to logfiles when an actual sync process is occurring\n*   Updated output of --display-config to be more relevant\n*   Updated manpage to align with application configuration\n*   Updated documentation and Docker files based on minimum compiler versions to dmd-2.088.0 and ldc-1.18.0\n*   Updated documentation (various)\n\n## 2.4.17 - 2022-04-30\n### Fixed\n*   Fix docker build, by add missing git package for Fedora builds\n*   Fix application crash when attempting to sync a broken symbolic link\n*   Fix Internet connect disruption retry handling and logging output\n*   Fix local folder creation timestamp with timestamp from OneDrive\n*   Fix logging output when download failed\n\n### Added\n*   Add additional logging specifically for delete event to denote in log output the source of a deletion event when running in --monitor mode\n\n### Changed\n*   Improve when the local database integrity check is performed and on what frequency the database integrity check is performed\n\n### Updated\n*   Remove application output ambiguity on how to access 'help' for the client\n*   Update logging output when running in --monitor --verbose mode in regards to the inotify events\n*   Updated documentation (various)\n\n## 2.4.16 - 2022-03-10\n### Fixed\n*   Update application file logging error handling\n*   Explicitly set libcurl options\n*   Fix that when a sync_list exclusion is matched, the item needs to be excluded when using --resync\n*   Fix so that application can be compiled correctly on Android hosts\n*   Fix the handling of 429 and 5xx responses when they are generated by OneDrive in a self-referencing circular pattern\n*   Fix applying permissions to volume directories when running in rootless podman\n*   Fix unhandled errors from OneDrive when initialising subscriptions fail\n\n### Added\n*   Enable GitHub Sponsors\n*   Implement --resync-auth to enable CLI passing in of --rsync approval\n*   Add function to check client version vs latest GitHub release\n*   Add --reauth to allow easy re-authentication of the client\n*   Implement --modified-by to display who last modified a file and when the modification was done\n*   Implement feature request to mark partially-downloaded files as .partial during download\n*   Add documentation for Podman support\n\n### Changed\n*   Document risk regarding using --resync and force user acceptance of usage risk to proceed\n*   Use YAML for Bug Reports and Feature Requests\n*   Update Dockerfiles to use more modern base Linux distribution\n\n### Updated\n*   Updated documentation (various)\n\n## 2.4.15 - 2021-12-31\n### Fixed\n*   Fix unable to upload to OneDrive Business Shared Folders due to OneDrive API restricting quota information\n*   Update fixing edge case with OneDrive Personal Shared Folders and --resync --upload-only\n\n### Added\n*   Add SystemD hardening\n*   Add --operation-timeout argument\n\n### Changed\n*   Updated minimum compiler versions to dmd-2.087.0 and ldc-1.17.0\n\n### Updated\n*   Updated Dockerfile-alpine to use Alpine 3.14\n*   Updated documentation (various)\n\n## 2.4.14 - 2021-11-24\n### Fixed\n*   Support DMD 2.097.0 as compiler for Docker Builds\n*   Fix getPathDetailsByDriveId query when using --dry-run and a nested path with --single-directory\n*   Fix edge case when syncing OneDrive Personal Shared Folders\n*   Catch unhandled API response errors when querying OneDrive Business Shared Folders\n*   Catch unhandled API response errors when listing OneDrive Business Shared Folders\n*   Fix error 'Key not found: remaining' with Business Shared Folders (OneDrive API change)\n*   Fix overwriting local files with older versions from OneDrive when items.sqlite3 does not exist and --resync is not used\n\n### Added\n*   Added operation_timeout as a new configuration to assist in cases where operations take longer that 1h to complete\n*   Add Real-Time syncing of remote updates via webhooks\n*   Add --auth-response option and expose through entrypoint.sh for Docker\n*   Add --disable-download-validation\n\n### Changed\n*   Always prompt for credentials for authentication rather than re-using cached browser details\n*   Do not re-auth on --logout\n\n### Updated\n*   Updated documentation (various)\n\n## 2.4.13 - 2021-7-14\n### Fixed\n*   Support DMD 2.097.0 as compiler\n*   Fix to handle OneDrive API Bad Request response when querying if file exists\n*   Fix application crash and incorrect handling of --single-directory when syncing a OneDrive Business Shared Folder due to using 'Add Shortcut to My Files'\n*   Fix application crash due to invalid UTF-8 sequence in the pathname for the application configuration\n*   Fix error message when deleting a large number of files\n*   Fix Docker build process to source GOSU keys from updated GPG key location\n*   Fix application crash due to a conversion overflow when calculating file offset for session uploads\n*   Fix Docker Alpine build failing due to filesystem permissions issue due to Docker build system and Alpine Linux 3.14 incompatibility\n*   Fix that Business Shared Folders with parentheses are ignored\n\n### Updated\n*   Updated Lock Bot to run daily\n*   Updated documentation (various)\n\n## 2.4.12 - 2021-5-28\n### Fixed\n*   Fix an unhandled Error 412 when uploading modified files to OneDrive Business Accounts\n*   Fix 'sync_list' handling of inclusions when name is included in another folders name\n*   Fix that options --upload-only & --remove-source-files are ignored on an upload session restore\n*   Fix to add file check when adding item to database if using --upload-only --remove-source-files\n*   Fix application crash when SharePoint displayName is being withheld\n\n### Updated\n*   Updated Lock Bot to use GitHub Actions\n*   Updated documentation (various)\n\n## 2.4.11 - 2021-4-07\n### Fixed\n*   Fix support for '/*' regardless of location within sync_list file\n*   Fix 429 response handling correctly check for 'retry-after' response header and use set value\n*   Fix 'sync_list' path handling for sub item matching, so that items in parent are not implicitly matched when there is no wildcard present\n*   Fix --get-O365-drive-id to use 'nextLink' value if present when searching for specific SharePoint site names\n*   Fix OneDrive Business Shared Folder existing name conflict check\n*   Fix incorrect error message 'Item cannot be deleted from OneDrive because it was not found in the local database' when item is actually present\n*   Fix application crash when unable to rename folder structure due to unhandled file-system issue\n*   Fix uploading documents to Shared Business Folders when the shared folder exists on a SharePoint site due to Microsoft Sharepoint 'enrichment' of files\n*   Fix that a file record is kept in database when using --no-remote-delete & --remove-source-files\n\n### Added\n*   Added support in --get-O365-drive-id to provide the 'drive_id' for multiple 'document libraries' within a single Shared Library Site\n\n### Removed\n*   Removed the deprecated config option 'force_http_11' which was flagged as deprecated by PR #549 in v2.3.6 (June 2019)\n\n### Updated\n*   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\n*   Updated Docker build files for Raspberry Pi to dedicated armhf & aarch64 Dockerfiles\n*   Updated logging output when in --monitor mode, avoid outputting misleading logging when the new or modified item is a file, not a directory\n*   Updated documentation (various)\n\n## 2.4.10 - 2021-2-19\n### Fixed\n*   Catch database assertion when item path cannot be calculated\n*   Fix alpine Docker build so it uses the same golang alpine version\n*   Search all distinct drive id's rather than just default drive id for --get-file-link\n*   Use correct driveId value to query for changes when using --single-directory\n*   Improve upload handling of files for SharePoint sites and detecting when SharePoint modifies the file post upload\n*   Correctly handle '~' when present in 'log_dir' configuration option\n*   Fix logging output when handing downloaded new files\n*   Fix to use correct path offset for sync_list exclusion matching \n\n### Added\n*   Add upload speed metrics when files are uploaded and clarify that 'data to transfer' is what is needed to be downloaded from OneDrive\n*   Add new config option to rate limit connection to OneDrive\n*   Support new file maximum upload size of 250GB\n*   Support sync_list matching full path root wildcard with exclusions to simplify sync_list configuration\n\n### Updated\n*   Rename Office365.md --> SharePoint-Shared-Libraries.md which better describes this document\n*   Updated Dockerfile config for arm64\n*   Updated documentation (various)\n\n## 2.4.9 - 2020-12-27\n### Fixed\n*   Fix to handle case where API provided deltaLink generates a further API error\n*   Fix application crash when unable to read a local file due to local file permissions\n*   Fix application crash when calculating the path length due to invalid UTF characters in local path\n*   Fix Docker build on Alpine due missing symbols due to using the edge version of ldc and ldc-runtime\n*   Fix application crash with --get-O365-drive-id when API response is restricted\n\n### Added\n*   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\n*   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\n*   Add Docker environment variable to allow --logout for re-authentication\n\n### Updated\n*   Remove duplicate code for error output functions and enhance error logging output\n*   Updated documentation\n\n## 2.4.8 - 2020-11-30\n### Fixed\n*   Fix to use config set option for 'remove_source_files' and 'skip_dir_strict_match' rather than ignore if set\n*   Fix download failure and crash due to incorrect local filesystem permissions when using mounted external devices\n*   Fix to not change permissions on pre-existing local directories\n*   Fix logging output when authentication authorisation fails to not say authorisation was successful\n*   Fix to check application_id before setting redirect URL when using specific Azure endpoints\n*   Fix application crash in --monitor mode due to 'Failed to stat file' when setgid is used on a directory and data cannot be read\n\n### Added\n*   Added advanced-usage.md to document advanced client usage such as multi account configurations and Windows dual-boot\n\n### Updated\n*   Updated --verbose logging output for config options when set\n*   Updated documentation (man page, USAGE.md, Office365.md, BusinessSharedFolders.md)\n\n## 2.4.7 - 2020-11-09\n### Fixed\n*   Fix debugging output for /delta changes available queries\n*   Fix logging output for modification comparison source data\n*   Fix Business Shared Folder handling to process only Shared Folders, not individually shared files\n*   Fix cleanup dryrun shm and wal files if they exist\n*   Fix --list-shared-folders to only show folders\n*   Fix to check for the presence of .nosync when processing DB entries\n*   Fix skip_dir matching when using --resync\n*   Fix uploading data to shared business folders when using --upload-only\n*   Fix to merge contents of SQLite WAL file into main database file on sync completion\n*   Fix to check if localModifiedTime is >= than item.mtime to avoid re-upload for equal modified time\n*   Fix to correctly set config directory permissions at first start\n\n### Added\n*   Added environment variable to allow easy HTTPS debug in docker\n*   Added environment variable to allow download-only mode in Docker\n*   Implement Feature: Allow config to specify a tenant id for non-multi-tenant applications\n*   Implement Feature: Adding support for authentication with single tenant custom applications\n*   Implement Feature: Configure specific File and Folder Permissions\n\n### Updated\n*   Updated documentation (readme.md, install.md, usage.md, bug_report.md)\n\n## 2.4.6 - 2020-10-04\n### Fixed\n*   Fix flagging of remaining free space when value is being restricted\n*   Fix --single-directory path handling when path does not exist locally\n*   Fix checking for 'Icon' path as no longer listed by Microsoft as an invalid file or folder name\n*   Fix removing child items on OneDrive when parent item responds with access denied\n*   Fix to handle deletion events for files when inotify events are missing\n*   Fix uninitialised value error as reported by valgrind\n*   Fix to handle deletion events for directories when inotify events are missing\n\n### Added\n*   Implement Feature: Create shareable link\n*   Implement Feature: Support wildcard within sync_list entries\n*   Implement Feature: Support negative patterns in sync_list for fine grained exclusions\n*   Implement Feature: Multiple skip_dir & skip_file configuration rules\n*   Add GUI notification to advise users when the client needs to be reauthenticated\n\n### Updated\n*   Updated documentation (readme.md, install.md, usage.md, bug_report.md)\n\n## 2.4.5 - 2020-08-13\n### Fixed\n*   Fixed fish auto completions installation destination\n\n## 2.4.4 - 2020-08-11\n### Fixed\n*   Fix 'skip_dir' & 'skip_file' pattern matching to ensure correct matching is performed\n*   Fix 'skip_dir' & 'skip_file' so that each directive is only used against directories or files as required in --monitor\n*   Fix client hand when attempting to sync a Unix pipe file\n*   Fix --single-directory & 'sync_list' performance \n*   Fix erroneous 'return' statements which could prematurely end processing all changes returned from OneDrive\n*   Fix segfault when attempting to perform a comparison on an inotify event when determining if event path is directory or file\n*   Fix handling of Shared Folders to ensure these are checked against 'skip_dir' entries\n*   Fix 'Skipping uploading this new file as parent path is not in the database' when uploading to a Personal Shared Folder\n*   Fix how available free space is tracked when uploading files to OneDrive and Shared Folders\n*   Fix --single-directory handling of parent path matching if path is being seen for first time\n\n### Added\n*   Added Fish auto completions\n\n### Updated\n*   Increase maximum individual file size to 100GB due to Microsoft file limit increase\n*   Update Docker build files and align version of compiler across all Docker builds\n*   Update Docker documentation\n*   Update NixOS build information\n*   Update the 'Processing XXXX' output to display the full path\n*   Update logging output when a sync starts and completes when using --monitor\n*   Update Office 365 / SharePoint site search query and response if query return zero match\n\n## 2.4.3 - 2020-06-29\n### Fixed\n*   Check if symbolic link is relative to location path\n*   When using output logfile, fix inconsistent output spacing\n*   Perform initial sync at startup in monitor mode\n*   Handle a 'race' condition to process inotify events generated whilst performing DB or filesystem walk\n*   Fix segfault when moving folder outside the sync directory when using --monitor on Arch Linux\n\n### Added\n*   Added additional inotify event debugging\n*   Added support for loading system configs if there's no user config\n*   Added Ubuntu installation details to include installing the client from a PPA\n*   Added openSUSE installation details to include installing the client from a package\n*   Added support for comments in sync_list file\n*   Implement recursive deletion when Retention Policy is enabled on OneDrive Business Accounts\n*   Implement support for National cloud deployments\n*   Implement OneDrive Business Shared Folders Support\n\n### Updated\n*   Updated documentation files (various)\n*   Updated log output messaging when a full scan has been set or triggered\n*   Updated buildNormalizedPath complexity to simplify code\n*   Updated to only process OneDrive Personal Shared Folders only if account type is 'personal'\n\n## 2.4.2 - 2020-05-27\n### Fixed\n*   Fixed the catching of an unhandled exception when inotify throws an error\n*   Fixed an uncaught '100 Continue' response when files are being uploaded\n*   Fixed progress bar for uploads to be more accurate regarding percentage complete\n*   Fixed handling of database query enforcement if item is from a shared folder\n*   Fixed compiler depreciation of std.digest.digest\n*   Fixed checking & loading of configuration file sequence\n*   Fixed multiple issues reported by Valgrind\n*   Fixed double scan at application startup when using --monitor & --resync together\n*   Fixed when renaming a file locally, ensure that the target filename is valid before attempting to upload to OneDrive\n*   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\n\n### Added\n*   Implement 'bypass_data_preservation' enhancement\n\n### Changed\n*   Changed the monitor interval default to 300 seconds\n\n### Updated\n*   Updated the handling of out-of-space message when OneDrive is out of space\n*   Updated debug logging for retry wait times\n\n## 2.4.1 - 2020-05-02\n### Fixed\n*   Fixed the handling of renaming files to a name starting with a dot when skip_dotfiles = true\n*   Fixed the handling of parentheses from path or file names, when doing comparison with regex\n*   Fixed the handling of renaming dotfiles to another dotfile when skip_dotfile=true in monitor mode\n*   Fixed the handling of --dry-run and --resync together correctly as current database may be corrupt\n*   Fixed building on Alpine Linux under Docker\n*   Fixed the handling of --single-directory for --dry-run and --resync scenarios\n*   Fixed the handling of .nosync directive when downloading new files into existing directories that is (was) in sync\n*   Fixed the handling of zero-byte modified files for OneDrive Business\n*   Fixed skip_dotfiles handling of .folders when in monitor mode to prevent monitoring\n*   Fixed the handling of '.folder' -> 'folder' move when skip_dotfiles is enabled\n*   Fixed the handling of folders that cannot be read (permission error) if parent should be skipped\n*   Fixed the handling of moving folders from skipped directory to non-skipped directory via OneDrive web interface\n*   Fixed building on CentOS Linux under Docker\n*   Fixed Codacy reported issues: double quote to prevent globbing and word splitting\n*   Fixed an assertion when attempting to compute complex path comparison from shared folders\n*   Fixed the handling of .folders when being skipped via skip_dir\n\n### Added\n*   Implement Feature: Implement the ability to set --resync as a config option, default is false\n\n### Updated\n*   Update error logging to be consistent when initialising fails\n*   Update error logging output to handle HTML error response reasoning if present\n*   Update link to new Microsoft documentation\n*   Update logging output to differentiate between OneNote objects and other unsupported objects\n*   Update RHEL/CentOS spec file example\n*   Update known-issues.md regarding 'SSL_ERROR_SYSCALL, errno 104'\n*   Update progress bar to be more accurate when downloading large files\n*   Updated #658 and #865 handling of when to trigger a directory walk when changes occur on OneDrive\n*   Updated handling of when a full scan is required due to utilising sync_list\n*   Updated handling of when OneDrive service throws a 429 or 504 response to retry original request after a delay\n\n## 2.4.0 - 2020-03-22\n### Fixed\n*   Fixed how the application handles 429 response codes from OneDrive (critical update)\n*   Fixed building on Alpine Linux under Docker\n*   Fixed how the 'username' is determined from the running process for logfile naming\n*   Fixed file handling when a failed download has occurred due to exiting via CTRL-C\n*   Fixed an unhandled exception when OneDrive throws an error response on initialising\n*   Fixed the handling of moving files into a skipped .folder when skip_dotfiles = true\n*   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.\n\n### Added\n*   Added a Dockerfile for building on Raspberry Pi / ARM platforms\n*   Implement Feature: warning on big deletes to safeguard data on OneDrive\n*   Implement Feature: delete local files after sync\n*   Implement Feature: perform skip_dir explicit match only\n*   Implement Feature: provide config file option for specifying the Client Identifier\n\n### Changed\n*   Updated the 'Client Identifier' to a new Application ID\n\n### Updated\n*   Updated relevant documentation (README.md, USAGE.md) to add new feature details and clarify existing information\n*   Update completions to include the --force-http-2 option\n*   Update to always log when a file is skipped due to the item being invalid\n*   Update application output when just authorising application to make information clearer\n*   Update logging output when using sync_list to be clearer as to what is actually being processed and why\n\n## 2.3.13 - 2019-12-31\n### Fixed\n*   Change the sync list override flag to false as default when not using sync_list\n*   Fix --dry-run output when using --upload-only & --no-remote-delete and deleting local files\n\n### Added\n*   Add a verbose log entry when a monitor sync loop with OneDrive starts & completes\n\n### Changed\n*   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\n\n### Updated\n*   Updated INSTALL.md with Ubuntu 16.x i386 build instructions to reflect working configuration on legacy hardware\n*   Updated INSTALL.md with details of Linux packages\n*   Updated INSTALL.md build instructions for CentOS platforms\n\n## 2.3.12 - 2019-12-04\n### Fixed\n*   Retry session upload fragment when transient errors occur to prevent silent upload failure\n*   Update Microsoft restriction and limitations about windows naming files to include '~' for folder names\n*   Docker guide fixes, add multiple account setup instructions\n*   Check database for excluded sync_list items previously in scope\n*   Catch DNS resolution error\n*   Fix where an item now out of scope should be flagged for local delete\n*   Fix rebuilding of onedrive, but ensure version is properly updated \n*   Update Ubuntu i386 build instructions to use DMD using preferred method\n\n### Added\n*   Add debug message to when a message is sent to dbus or notification daemon\n*   Add i386 instructions for legacy low memory platforms using LDC\n\n## 2.3.11 - 2019-11-05\n### Fixed\n*   Fix typo in the documentation regarding invalid config when upgrading from 'skilion' codebase\n*   Fix handling of skip_dir, skip_file & sync_list config options\n*   Fix typo in the documentation regarding sync_list\n*   Fix log output to be consistent with sync_list exclusion\n*   Fix 'Processing X changes' output to be more reflective of actual activity when using sync_list\n*   Remove unused and unexported SED variable in Makefile.in \n*   Handle curl exceptions and timeouts better with backoff/retry logic\n*   Update skip_dir pattern matching when using wildcards\n*   Fix when a full rescan is performed when using sync_list\n*   Fix 'Key not found: name' when computing skip_dir path\n*   Fix call from --monitor to observe --no-remote-delete\n*   Fix unhandled exception when monitor initialisation failure occurs due to too many open local files\n*   Fix unhandled 412 error response from OneDrive API when moving files right after upload\n*   Fix --monitor when used with --download-only. This fixes a regression introduced in 12947d1.\n*   Fix if --single-directory is being used, and we are using --monitor, only set inotify watches on the single directory\n\n### Changed\n*   Move JSON logging output from error messages to debug output\n\n## 2.3.10 - 2019-10-01\n### Fixed\n*   Fix searching for 'name' when deleting a synced item, if the OneDrive API does not return the expected details in the API call\n*   Fix abnormal termination when no Internet connection\n*   Fix downloading of files from OneDrive Personal Shared Folders when the OneDrive API responds with unexpected additional path data\n*   Fix logging of 'initialisation' of client to actually when the attempt to initialise is performed\n*   Fix when using a sync_list file, using deltaLink will actually 'miss' changes (moves & deletes) on OneDrive as using sync_list discards changes\n*   Fix OneDrive API status code 500 handling when uploading files as error message is not correct\n*   Fix crash when resume_upload file is not a valid JSON \n*   Fix crash when a file system exception is generated when attempting to update the file date & time and this fails\n\n### Added\n*   If there is a case-insensitive match error, also return the remote name from the response\n*   Make user-agent string a configuration option & add to config file\n*   Set default User-Agent to 'OneDrive Client for Linux v{version}'\n\n### Changed\n*   Make verbose logging output optional on Docker\n*   Enable --resync & debug client output via environment variables on Docker\n\n## 2.3.9 - 2019-09-01\n### Fixed\n*   Catch a 403 Forbidden exception when querying Sharepoint Library Names\n*   Fix unhandled error exceptions that cause application to exit / crash when uploading files\n*   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\n*   Fix handling of 5xx responses from OneDrive when uploading via a session\n\n### Added\n*   Detect the need for --resync when config changes either via config file or cli override\n\n### Changed\n*   Change minimum required version of LDC to v1.12.0\n\n### Removed\n*   Remove redundant logging output due to change in how errors are reported from OneDrive\n\n## 2.3.8 - 2019-08-04\n### Fixed\n*   Fix unable to download all files when OneDrive fails to return file level details used to validate file integrity\n*   Included the flag \"-m\" to create the home directory when creating the user\n*   Fix entrypoint.sh to work with \"sudo docker run\"\n*   Fix docker build error on stretch\n*   Fix hidden directories in 'root' from having prefix removed\n*   Fix Sharepoint Document Library handling for .txt & .csv files\n*   Fix logging for init.d service\n*   Fix OneDrive response missing required 'id' element when uploading images\n*   Fix 'Unexpected character '<'. (Line 1:1)' when OneDrive has an exception error\n*   Fix error when creating the sync dir fails when there is no permission to create the sync dir\n\n### Added\n*   Add explicit check for hashes to be returned in cases where OneDrive API fails to provide them despite requested to do so\n*   Add comparison with sha1 if OneDrive provides that rather than quickXor\n*   Add selinux configuration details for a sync folder outside of the home folder\n*   Add date tag on docker.hub\n*   Add back CentOS 6 install & uninstall to Makefile\n*   Add a check to handle moving items out of sync_list sync scope & delete locally if true\n*   Implement --get-file-link which will return the weburl of a file which has been synced to OneDrive\n\n### Changed\n*   Change unauthorized-api exit code to 3\n*   Update LDC to v1.16.0 for Travis CI testing\n*   Use replace function for modified Sharepoint Document Library files rather than delete and upload as new file, preserving file history\n*   Update Sharepoint modified file handling for files > 4Mb in size\n\n### Removed\n*   Remove -d shorthand for --download-only to avoid confusion with other GNU applications where -d stands for 'debug'\n\n## 2.3.7 - 2019-07-03\n### Fixed\n*   Fix not all files being downloaded due to OneDrive query failure\n*   False DB update which potentially could had lead to false data loss on OneDrive\n\n## 2.3.6 - 2019-07-03 (DO NOT USE)\n### Fixed\n*   Fix JSONValue object validation\n*   Fix building without git being available\n*   Fix some spelling/grammatical errors\n*   Fix OneDrive error response on creating upload session\n\n### Added\n*   Add download size & hash check to ensure downloaded files are valid and not corrupt\n*   Added --force-http-2 to use HTTP/2 if desired\n\n### Changed\n*   Deprecated --force-http-1.1 (enabled by default) due to OneDrive inconsistent behavior with HTTP/2 protocol\n\n## 2.3.5 - 2019-06-19\n### Fixed\n*   Handle a directory in the sync_dir when no permission to access\n*   Get rid of forced root necessity during installation\n*   Fix broken autoconf code for --enable-XXX options\n*   Fix so that skip_size check should only be used if configured\n*   Fix a OneDrive Internal Error exception occurring before attempting to download a file\n\n### Added\n*   Check for supported version of D compiler\n\n## 2.3.4 - 2019-06-13\n### Fixed\n*   Fix 'Local files not deleted' when using bad 'skip_file' entry\n*   Fix --dry-run logging output for faking downloading new files\n*   Fix install unit files to correct location on RHEL/CentOS 7\n*   Fix up unit file removal on all platforms\n*   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\n*   Fix an unhandled curl exception when OneDrive throws an internal timeout error\n*   Check timestamp to ensure that latest timestamp is used when comparing OneDrive changes\n*   Fix handling responses where cTag JSON elements are missing\n*   Fix Docker entrypoint.sh failures when GID is defined but not UID\n\n### Added\n*   Add autoconf based build system\n*   Add an encoding validation check before any path length checks are performed as if the path contains any invalid UTF-8 sequences\n*   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\n*   Implement skip_size feature request\n*   Implement feature request to support file based OneDrive authorization (request | response)\n\n### Updated\n*   Better handle initialisation issues when OneDrive / MS Graph is experiencing problems that generate 401 & 5xx error codes\n*   Enhance error message when unable to connect to Microsoft OneDrive service when the local CA SSL certificate(s) have issues\n*   Update Dockerfile to correctly build on Docker Hub\n*   Rework directory layout and re-factor MD files for readability\n\n## 2.3.3 - 2019-04-16\n### Fixed\n*   Fix --upload-only check for Sharepoint uploads\n*   Fix check to ensure item root we flag as 'root' actually is OneDrive account 'root'\n*   Handle object error response from OneDrive when uploading to OneDrive Business\n*   Fix handling of some OneDrive accounts not providing 'quota' details\n*   Fix 'resume_upload' handling in the event of bad OneDrive response\n\n### Added\n*   Add debugging for --get-O365-drive-id function\n*   Add shell (bash,zsh) completion support\n*   Add config options for command line switches to allow for better config handling in docker containers\n\n### Updated\n*   Implement more meaningful 5xx error responses\n*   Update onedrive.logrotate indentations and comments\n*   Update 'min_notif_changes' to 'min_notify_changes'\n\n## 2.3.2 - 2019-04-02\n### Fixed\n*   Reduce scanning the entire local system in monitor mode for local changes\n*   Resolve file creation loop when working directly in the synced folder and Microsoft Sharepoint\n\n### Added\n*   Add 'monitor_fullscan_frequency' config option to set the frequency of performing a full disk scan when in monitor mode\n\n### Updated\n*   Update default 'skip_file' to include tmp and lock files generated by LibreOffice\n*   Update database version due to changing defaults of 'skip_file' which will force a rebuild and use of new skip_file default regex\n\n## 2.3.1 - 2019-03-26\n### Fixed\n*   Resolve 'make install' issue where rebuild of application would occur due to 'version' being flagged as .PHONY\n*   Update readme build instructions to include 'make clean;' before build to ensure that 'version' is cleanly removed and can be updated correctly\n*   Update Debian Travis CI build URL's\n\n## 2.3.0 - 2019-03-25\n### Fixed\n*   Resolve application crash if no 'size' value is returned when uploading a new file\n*   Resolve application crash if a 5xx error is returned when uploading a new file\n*   Resolve not 'refreshing' version file when rebuilding\n*   Resolve unexpected application processing by preventing use of --synchronize & --monitor together\n*   Resolve high CPU usage when performing DB reads\n*   Update error logging around directory case-insensitive match\n*   Update Travis CI and ARM dependencies for LDC 1.14.0\n*   Update Makefile due to build failure if building from release archive file\n*   Update logging as to why a OneDrive object was skipped\n\n### Added\n*   Implement config option 'skip_dir'\n\n## 2.2.6 - 2019-03-12\n### Fixed\n*   Resolve application crash when unable to delete remote folders when business retention policies are enabled\n*   Resolve deprecation warning: loop index implicitly converted from size_t to int\n*   Resolve warnings regarding 'bashisms'\n*   Resolve handling of notification failure is dbus server has not started or available\n*   Resolve handling of response JSON to ensure that 'id' key element is always checked for\n*   Resolve excessive & needless logging in monitor mode\n*   Resolve compiling with LDC on Alpine as musl lacks some standard interfaces\n*   Resolve notification issues when offline and cannot act on changes\n*   Resolve Docker entrypoint.sh to accept command line arguments\n*   Resolve to create a new upload session on reinit \n*   Resolve where on OneDrive query failure, default root and drive id is used if a response is not returned\n*   Resolve Key not found: nextExpectedRanges when attempting session uploads and incorrect response is returned\n*   Resolve application crash when re-using an authentication URI twice after previous --logout\n*   Resolve creating a folder on a shared personal folder appears successful but returns a JSON error\n*   Resolve to treat mv of new file as upload of mv target\n*   Update Debian i386 build dependencies\n*   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\n*   Update Docker readme & documentation\n*   Update handling of validating local file permissions for new file uploads\n### Added\n*   Add support for install & uninstall on RHEL / CentOS 6.x\n*   Add support for when notifications are enabled, display the number of OneDrive changes to process if any are found\n*   Add 'config' option 'min_notif_changes' for minimum number of changes to notify on, default = 5\n*   Add additional Docker container builds utilising a smaller OS footprint\n*   Add configurable interval of logging in monitor mode\n*   Implement new CLI option --skip-dot-files to skip .files and .folders if option is used\n*   Implement new CLI option --check-for-nosync to ignore folder when special file (.nosync) present\n*   Implement new CLI option --dry-run\n\n## 2.2.5 - 2019-01-16\n### Fixed\n*   Update handling of HTTP 412 - Precondition Failed errors\n*   Update --display-config to display sync_list if configured\n*   Add a check for 'id' key on metadata update to prevent 'std.json.JSONException@std/json.d(494): Key not found: id'\n*   Update handling of 'remote' folder designation as 'root' items\n*   Ensure that remote deletes are handled correctly\n*   Handle 'Item not found' exception when unable to query OneDrive 'root' for changes\n*   Add handling for JSON response error when OneDrive API returns a 404 due to OneDrive API regression\n*   Fix items highlighted by codacy review\n### Added\n*   Add --force-http-1.1 flag to downgrade any HTTP/2 curl operations to HTTP 1.1 protocol\n*   Support building with ldc2 and usage of pkg-config for lib finding\n\n## 2.2.4 - 2018-12-28\n### Fixed\n*   Resolve JSONException when supplying --get-O365-drive-id option with a string containing spaces\n*   Resolve 'sync_dir' not read from 'config' file when run in Docker container\n*   Resolve logic where potentially a 'default' ~/OneDrive sync_dir could be set despite 'config' file configured for an alternate\n*   Make sure sqlite checkpointing works by properly finalizing statements\n*   Update logic handling of --single-directory to prevent inadvertent local data loss\n*   Resolve signal handling and database shutdown on SIGINT and SIGTERM\n*   Update man page\n*   Implement better help output formatting\n### Added\n*   Add debug handling for sync_dir operations\n*   Add debug handling for homePath calculation\n*   Add debug handling for configDirBase calculation\n*   Add debug handling if syncDir is created\n*   Implement Feature Request: Add status command or switch\n\n## 2.2.3 - 2018-12-20\n### Fixed\n*   Fix syncdir option is ignored\n\n## 2.2.2 - 2018-12-20\n### Fixed\n*   Handle short lived files in monitor mode\n*   Provide better log messages, less noise on temporary timeouts\n*   Deal with items that disappear during upload\n*   Deal with deleted move targets\n*   Reinitialize sync engine after three failed attempts\n*   Fix activation of dmd for docker builds\n*   Fix to check displayName rather than description for --get-O365-drive-id\n*   Fix checking of config file keys for validity\n*   Fix exception handling when missing parameter from usage option\n### Added\n*   Notification support via libnotify\n*   Add very verbose (debug) mode by double -v -v\n*   Implement option --display-config\n\n## 2.2.1 - 2018-12-04\n### Fixed\n*   Gracefully handle connection errors in monitor mode \n*   Fix renaming of files when syncing \n*   Installation of doc files, addition of man page \n*   Adjust timeout values for libcurl \n*   Continue in monitor mode when sync timed out \n*   Fix unreachable statements \n*   Update Makefile to better support packaging \n*   Allow starting offline in monitor mode \n### Added\n*   Implement --get-O365-drive-id to get correct SharePoint Shared Library (#248)\n*   Docker buildfiles for onedrive service (#262) \n\n## 2.2.0 - 2018-11-24\n### Fixed\n*   Updated client to output additional logging when debugging\n*   Resolve database assertion failure due to authentication\n*   Resolve unable to create folders on shared OneDrive Personal accounts\n### Added\n*   Implement feature request to Sync from Microsoft SharePoint\n*   Implement feature request to specify a logging directory if logging is enabled\n### Changed\n*   Change '--download' to '--download-only' to align with '--upload-only'\n*   Change logging so that logging to a separate file is no longer the default\n\n## 2.1.6 - 2018-11-15\n### Fixed\n*   Updated HTTP/2 transport handling when using curl 7.62.0 for session uploads\n### Added\n*   Added PKGBUILD for makepkg for building packages under Arch Linux\n\n## 2.1.5 - 2018-11-11\n### Fixed\n*   Resolve 'Key not found: path' when syncing from some shared folders due to OneDrive API change\n*   Resolve to only upload changes on remote folder if the item is in the database - dont assert if false\n*   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\n*   Resolve to handle HTTP request returned status code 412 (Precondition Failed) for session uploads to OneDrive Personal Accounts\n*   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\n*   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\n### Updated\n*   Updated Travis CI building on LDC v1.11.0 for ARMHF builds\n*   Updated Makefile to use 'install -D -m 644' rather than 'cp -raf'\n*   Updated default config to be aligned to code defaults\n\n## 2.1.4 - 2018-10-10\n### Fixed\n*   Resolve syncing of OneDrive Personal Shared Folders due to OneDrive API change\n*   Resolve incorrect systemd installation location(s) in Makefile\n\n## 2.1.3 - 2018-10-04\n### Fixed\n*   Resolve File download fails if the file is marked as malware in OneDrive\n*   Resolve high CPU usage when running in monitor mode\n*   Resolve how default path is set when running under systemd on headless systems\n*   Resolve incorrectly nested configDir in X11 systems\n*   Resolve Key not found: driveType\n*   Resolve to validate filename length before download to conform with Linux FS limits\n*   Resolve file handling to look for HTML ASCII codes which will cause uploads to fail\n*   Resolve Key not found: expirationDateTime on session resume\n### Added\n*   Update Travis CI building to test build on ARM64\n\n## 2.1.2 - 2018-08-27\n### Fixed\n*   Resolve skipping of symlinks in monitor mode\n*   Resolve Gateway Timeout - JSONValue is not an object\n*   Resolve systemd/user is not supported on CentOS / RHEL\n*   Resolve HTTP request returned status code 429 (Too Many Requests)\n*   Resolve handling of maximum path length calculation\n*   Resolve 'The parent item is not in the local database'\n*   Resolve Correctly handle file case sensitivity issues in same folder\n*   Update unit files documentation link\n\n## 2.1.1 - 2018-08-14\n### Fixed\n*   Fix handling no remote delete of remote directories when using --no-remote-delete\n*   Fix handling of no permission to access a local file / corrupt local file\n*   Fix application crash when unable to access login.microsoft.com upon application startup\n### Added\n*   Build instructions for openSUSE Leap 15.0\n\n## 2.1.0 - 2018-08-10\n### Fixed\n*   Fix handling of database exit scenarios when there is zero disk space left on drive where the items database resides\n*   Fix handling of incorrect database permissions\n*   Fix handling of different database versions to automatically re-create tables if version mis-match\n*   Fix handling timeout when accessing the Microsoft OneDrive Service\n*   Fix localFileModifiedTime to not use fraction seconds\n### Added\n*   Implement Feature: Add a progress bar for large uploads & downloads\n*   Implement Feature: Make checkinterval for monitor configurable\n*   Implement Feature: Upload Only Option that does not perform remote delete\n*   Implement Feature: Add ability to skip symlinks\n*   Add dependency, ebuild and build instructions for Gentoo distributions\n### Changed\n*   Build instructions for x86, x86_64 and ARM32 platforms\n*   Travis CI files to automate building on x32, x64 and ARM32 architectures\n*   Travis CI files to test built application against valid, invalid and problem files from previous issues\n\n## 2.0.2 - 2018-07-18\n### Fixed\n*   Fix systemd service install for builds with DESTDIR defined\n*   Fix 'HTTP 412 - Precondition Failed' error handling\n*   Gracefully handle OneDrive account password change\n*   Update logic handling of --upload-only and --local-first\n\n## 2.0.1 - 2018-07-11\n### Fixed\n*   Resolve computeQuickXorHash generates a different hash when files are > 64Kb\n\n## 2.0.0 - 2018-07-10\n### Fixed\n*   Resolve conflict resolution issue during syncing - the client does not handle conflicts very well & keeps on adding the hostname to files\n*   Resolve skilion #356 by adding additional check for 409 response from OneDrive\n*   Resolve multiple versions of file shown on website after single upload\n*   Resolve to gracefully fail when 'onedrive' process cannot get exclusive database lock\n*   Resolve 'Key not found: fileSystemInfo' when then item is a remote item (OneDrive Personal)\n*   Resolve skip_file config entry needs to be checked for any characters to escape\n*   Resolve Microsoft Naming Convention not being followed correctly\n*   Resolve Error when trying to upload a file with weird non printable characters present\n*   Resolve Crash if file is locked by online editing (status code 423)\n*   Resolve compilation issue with dmd-2.081.0\n*   Resolve skip_file configuration doesn't handle spaces or specified directory paths\n### Added\n*   Implement Feature: Add a flag to detect when the sync-folder is missing\n*   Implement Travis CI for code testing\n### Changed\n*   Update Makefile to use DESTDIR variables\n*   Update OneDrive Business maximum path length from 256 to 400\n*   Update OneDrive Business allowed characters for files and folders\n*   Update sync_dir handling to use the absolute path for setting parameter to something other than ~/OneDrive via config file or command line\n*   Update Fedora build instructions\n\n## 1.1.2 - 2018-05-17\n### Fixed\n*   Fix 4xx errors including (412 pre-condition, 409 conflict)\n*   Fix Key not found: lastModifiedDateTime (OneDrive API change)\n*   Fix configuration directory not found when run via init.d\n*   Fix skilion Issues #73, #121, #132, #224, #257, #294, #295, #297, #298, #300, #306, #315, #320, #329, #334, #337, #341\n### Added\n*   Add logging - log client activities to a file (/var/log/onedrive/%username%.onedrive.log or ~/onedrive.log)\n*   Add https debugging as a flag\n*   Add `--synchronize` to prevent from syncing when just blindly running the application\n*   Add individual folder sync\n*   Add sync from local directory first rather than download first then upload\n*   Add upload long path check\n*   Add upload only\n*   Add check for max upload file size before attempting upload\n*   Add systemd unit files for single & multi user configuration\n*   Add init.d file for older init.d based services\n*   Add Microsoft naming conventions and namespace validation for items that will be uploaded\n*   Add remaining free space counter at client initialisation to avoid out of space upload issue\n*   Add large file upload size check to align to OneDrive file size limitations\n*   Add upload file size validation & retry if does not match\n*   Add graceful handling of some fatal errors (OneDrive 5xx error handling)\n\n## Unreleased - 2018-02-19\n### Fixed\n*   Crash when the delta link is expired\n### Changed\n*   Disabled buffering on stdout\n\n## 1.1.1 - 2018-01-20\n### Fixed\n*   Wrong regex for parsing authentication uri\n\n## 1.1.0 - 2018-01-19\n### Added\n*   Support for shared folders (OneDrive Personal only)\n*   `--download` option to only download changes\n*   `DC` variable in Makefile to chose the compiler\n### Changed\n*   Print logs on stdout instead of stderr\n*   Improve log messages\n\n## 1.0.1 - 2017-08-01\n### Added\n*   `--syncdir` option\n### Changed\n*   `--version` output simplified\n*   Updated README\n### Fixed\n*   Fix crash caused by remotely deleted and recreated directories\n\n## 1.0.0 - 2017-07-14\n### Added\n*   `--version` option\n"
  },
  {
    "path": "config",
    "content": "# Configuration for OneDrive Linux Client\n# This file contains the list of supported configuration fields with their default values.\n# All values need to be enclosed in quotes\n# When changing a config option below, remove the '#' from the start of the config line\n# For a more detailed explanation of all config options below see docs/application-config-options.md or the man page.\n\n## This is the config option for application id that used to identify itself to Microsoft OneDrive. \n#application_id = \"d50ca740-c83f-4d1b-b616-12c519384f0c\"\n\n## 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.\n#azure_ad_endpoint = \"\"\n\n## 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\".\n#azure_tenant_id = \"\"\n\n## 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.\n#bypass_data_preservation = \"false\"\n\n## 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.\n#check_nomount = \"false\"\n\n## 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.\n#check_nosync = \"false\"\n\n## 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.\n#classify_as_big_delete = \"1000\"\n\n## This config option provides the capability to cleanup local files and folders if they are removed online.\n#cleanup_local_files = \"false\"\n\n## This configuration setting manages the TCP connection timeout duration in seconds for HTTPS connections to Microsoft OneDrive when using the curl library.\n#connect_timeout = \"10\"\n\n## 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.\n#create_new_file_version = \"false\"\n\n## This setting controls the timeout duration, in seconds, for when data is not received on an active connection to Microsoft OneDrive over HTTPS.\n#data_timeout = \"60\"\n\n## This setting controls whether the curl library is configured to output additional data to assist with diagnosing HTTPS issues and problems.\n#debug_https = \"false\"\n\n## This setting controls whether 'inotify' events should be delayed or not.\n#delay_inotify_processing = \"false\"\n\n## This option determines whether the client will conduct integrity validation on files downloaded from Microsoft OneDrive.\n#disable_download_validation = \"false\"\n\n## This setting controls whether GUI notifications are sent from the client to your display manager session.\n#disable_notifications = \"false\"\n\n## 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'.\n#disable_permission_set = \"false\"\n\n## This option determines whether the client will conduct integrity validation on files uploaded to Microsoft OneDrive.\n#disable_upload_validation = \"false\"\n\n## This option will include the running config of the application at application startup.\n#display_running_config = \"false\"\n\n## This option will display file transfer metrics when enabled.\n#display_transfer_metrics = \"false\"\n\n## This setting controls the libcurl DNS cache value.\n#dns_timeout = \"60\"\n\n## This setting forces the client to only download data from Microsoft OneDrive and replicate that data locally.\n#download_only = \"false\"\n\n## This setting controls the specific drive identifier the client will use when syncing with Microsoft OneDrive.\n#drive_id = \"\"\n\n## This setting controls the application capability to test your application configuration without actually performing any real activity.\n#dry_run = \"false\"\n\n## This setting controls the application logging all actions to a separate file.\n#enable_logging = \"false\"\n\n## This setting controls the file fragment size when uploading large files to Microsoft OneDrive. \n#file_fragment_size = \"10\"\n\n## This setting controls the application HTTP protocol version, downgrading to HTTP/1.1 when enabled.\n#force_http_11 = \"false\"\n\n## This option, when enabled, forces the client to use a 'session' upload, which, when the 'file' is uploaded by the session, this includes the local timestamp of the file\n#force_session_upload = \"false\"\n\n## This setting controls the application IP protocol used when communicating with Microsoft OneDrive.\n#ip_protocol_version = \"0\"\n\n## This setting controls what the application considers the 'source of truth' for your data.\n#local_first = \"false\"\n\n## This setting controls the custom application log path when 'enable_logging' has been enabled.\n#log_dir = \"\"\n\n## This configuration option controls the number of seconds a cURL engine is considered stale and destroyed after last use.\n#max_curl_idle = \"120\"\n\n## This configuration option controls how often a full scan of your data is performed in monitor mode.\n#monitor_fullscan_frequency = \"12\"\n\n## This setting determines how often the sync loop runs in --monitor mode.\n#monitor_interval = \"300\"\n\n## This configuration option controls suppression of frequent monitor log messages.\n#monitor_log_frequency = \"12\"\n\n## This configuration option controls whether local deletes are replicated to OneDrive when using --upload-only.\n#no_remote_delete = \"false\"\n\n## This setting controls whether the client logs GUI notifications when file actions occur.\n#notify_file_actions = \"false\"\n\n## This configuration controls the maximum amount of time a file operation is allowed to take.\n#operation_timeout = \"3600\"\n\n## Permanently delete online items when removed locally. Bypasses OneDrive recycle bin.\n#permanent_delete = \"false\"\n\n## This setting limits the per-thread bandwidth used by the client.\n#rate_limit = \"0\"\n\n## This configuration option controls whether the client operates in read-only mode.\n#read_only_auth_scope = \"false\"\n\n## This configuration option allows you to specify the 'Recycle Bin' path for the application. This is only used if 'use_recycle_bin' is enabled.\n#recycle_bin_path = \"/path/to/desired/location/\"\n\n## This option removes the local file after a successful upload to OneDrive.\n#remove_source_files = \"false\"\n\n## This configuration controls whether a full resync is performed at application startup.\n#resync = \"false\"\n\n## This option approves use of --resync, useful in automated environments.\n#resync_auth = \"false\"\n\n## This option controls which directories are excluded from sync.\n#skip_dir = \"\"\n\n## When enabled, skip_dir matches must be strict, full path matches only.\n#skip_dir_strict_match = \"false\"\n\n## When enabled, skip dotfiles and dot folders from sync.\n#skip_dotfiles = \"false\"\n\n## This setting controls which files are skipped during sync.\n#skip_file = \"~*|.~*|*.tmp|*.swp|*.partial\"\n\n## Skip syncing files larger than this size in MB.\n#skip_size = \"0\"\n\n## Skip symbolic links during sync.\n#skip_symlinks = \"false\"\n\n## Reserve this much free disk space (in MB) to avoid disk full issues.\n#space_reservation = \"50\"\n\n## Sync OneDrive Business shared folders that are shortcuts in 'My Files'. These will be stored in a local folder called 'Files Shared With Me'.\n#sync_business_shared_items = \"false\"\n\n## Local directory to sync with OneDrive.\n#sync_dir = \"~/OneDrive\"\n\n## Permissions to apply to created local directories.\n#sync_dir_permissions = \"700\"\n\n## Permissions to apply to created local files.\n#sync_file_permissions = \"600\"\n\n## Sync all root files in sync_dir when using sync_list.\n#sync_root_files = \"false\"\n\n## Number of threads to use for upload/download.\n#threads = \"8\"\n\n## File transfer ordering between client and OneDrive.\n#transfer_order = \"default\"\n\n## Only upload changes to OneDrive, do not download from cloud.\n#upload_only = \"false\"\n\n## Authenticate using the Microsoft OAuth2 Device Authorisation Flow\n#use_device_auth = \"true\"\n\n## Single Sign-On (SSO) via Intune using the Microsoft Identity Device Broker\n#use_intune_sso = \"true\"\n\n## This configuration option controls the application function to move online deleted files to a 'Recycle Bin' on your system.\n#use_recycle_bin = \"false\"\n\n## Custom User-Agent string for requests to OneDrive. If you change this, you will get throttled by the Microsoft Graph API. Change with caution.\n#user_agent = \"ISV|abraunegg|OneDrive Client for Linux/vX.Y.Z-A-bcdefghi\"\n\n## Enable webhook-based remote update notifications in monitor mode.\n#webhook_enabled = \"false\"\n\n## Time in seconds before webhook subscription expires.\n#webhook_expiration_interval = \"600\"\n\n## IP address to listen on for incoming webhook updates.\n#webhook_listening_host = \"0.0.0.0\"\n\n## TCP port to listen on for incoming webhook updates.\n#webhook_listening_port = \"8888\"\n\n## Public webhook URL for Microsoft to send notifications to.\n#webhook_public_url = \"\"\n\n## Frequency (in seconds) to renew webhook subscription.\n#webhook_renewal_interval = \"300\"\n\n## Frequency (in seconds) to retry a failed webhook subscription renewal.\n#webhook_retry_interval = \"60\"\n\n## Write xattr metadata fields (createdBy, lastModifiedBy) to synced files.\n#write_xattr_data = \"false\"\n"
  },
  {
    "path": "configure",
    "content": "#! /bin/sh\n# Guess values for system-dependent variables and create Makefiles.\n# Generated by GNU Autoconf 2.69 for onedrive v2.5.10.\n#\n# Report bugs to <https://github.com/abraunegg/onedrive>.\n#\n#\n# Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc.\n#\n#\n# This configure script is free software; the Free Software Foundation\n# gives unlimited permission to copy, distribute and modify it.\n## -------------------- ##\n## M4sh Initialization. ##\n## -------------------- ##\n\n# Be more Bourne compatible\nDUALCASE=1; export DUALCASE # for MKS sh\nif test -n \"${ZSH_VERSION+set}\" && (emulate sh) >/dev/null 2>&1; then :\n  emulate sh\n  NULLCMD=:\n  # Pre-4.2 versions of Zsh do word splitting on ${1+\"$@\"}, which\n  # is contrary to our usage.  Disable this feature.\n  alias -g '${1+\"$@\"}'='\"$@\"'\n  setopt NO_GLOB_SUBST\nelse\n  case `(set -o) 2>/dev/null` in #(\n  *posix*) :\n    set -o posix ;; #(\n  *) :\n     ;;\nesac\nfi\n\n\nas_nl='\n'\nexport as_nl\n# Printing a long string crashes Solaris 7 /usr/bin/printf.\nas_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\nas_echo=$as_echo$as_echo$as_echo$as_echo$as_echo\nas_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo\n# Prefer a ksh shell builtin over an external printf program on Solaris,\n# but without wasting forks for bash or zsh.\nif test -z \"$BASH_VERSION$ZSH_VERSION\" \\\n    && (test \"X`print -r -- $as_echo`\" = \"X$as_echo\") 2>/dev/null; then\n  as_echo='print -r --'\n  as_echo_n='print -rn --'\nelif (test \"X`printf %s $as_echo`\" = \"X$as_echo\") 2>/dev/null; then\n  as_echo='printf %s\\n'\n  as_echo_n='printf %s'\nelse\n  if test \"X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`\" = \"X-n $as_echo\"; then\n    as_echo_body='eval /usr/ucb/echo -n \"$1$as_nl\"'\n    as_echo_n='/usr/ucb/echo -n'\n  else\n    as_echo_body='eval expr \"X$1\" : \"X\\\\(.*\\\\)\"'\n    as_echo_n_body='eval\n      arg=$1;\n      case $arg in #(\n      *\"$as_nl\"*)\n\texpr \"X$arg\" : \"X\\\\(.*\\\\)$as_nl\";\n\targ=`expr \"X$arg\" : \".*$as_nl\\\\(.*\\\\)\"`;;\n      esac;\n      expr \"X$arg\" : \"X\\\\(.*\\\\)\" | tr -d \"$as_nl\"\n    '\n    export as_echo_n_body\n    as_echo_n='sh -c $as_echo_n_body as_echo'\n  fi\n  export as_echo_body\n  as_echo='sh -c $as_echo_body as_echo'\nfi\n\n# The user is always right.\nif test \"${PATH_SEPARATOR+set}\" != set; then\n  PATH_SEPARATOR=:\n  (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {\n    (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||\n      PATH_SEPARATOR=';'\n  }\nfi\n\n\n# IFS\n# We need space, tab and new line, in precisely that order.  Quoting is\n# there to prevent editors from complaining about space-tab.\n# (If _AS_PATH_WALK were called with IFS unset, it would disable word\n# splitting by setting IFS to empty value.)\nIFS=\" \"\"\t$as_nl\"\n\n# Find who we are.  Look in the path if we contain no directory separator.\nas_myself=\ncase $0 in #((\n  *[\\\\/]* ) as_myself=$0 ;;\n  *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\nfor as_dir in $PATH\ndo\n  IFS=$as_save_IFS\n  test -z \"$as_dir\" && as_dir=.\n    test -r \"$as_dir/$0\" && as_myself=$as_dir/$0 && break\n  done\nIFS=$as_save_IFS\n\n     ;;\nesac\n# We did not find ourselves, most probably we were run as `sh COMMAND'\n# in which case we are not to be found in the path.\nif test \"x$as_myself\" = x; then\n  as_myself=$0\nfi\nif test ! -f \"$as_myself\"; then\n  $as_echo \"$as_myself: error: cannot find myself; rerun with an absolute file name\" >&2\n  exit 1\nfi\n\n# Unset variables that we do not need and which cause bugs (e.g. in\n# pre-3.0 UWIN ksh).  But do not cause bugs in bash 2.01; the \"|| exit 1\"\n# suppresses any \"Segmentation fault\" message there.  '((' could\n# trigger a bug in pdksh 5.2.14.\nfor as_var in BASH_ENV ENV MAIL MAILPATH\ndo eval test x\\${$as_var+set} = xset \\\n  && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || :\ndone\nPS1='$ '\nPS2='> '\nPS4='+ '\n\n# NLS nuisances.\nLC_ALL=C\nexport LC_ALL\nLANGUAGE=C\nexport LANGUAGE\n\n# CDPATH.\n(unset CDPATH) >/dev/null 2>&1 && unset CDPATH\n\n# Use a proper internal environment variable to ensure we don't fall\n  # into an infinite loop, continuously re-executing ourselves.\n  if test x\"${_as_can_reexec}\" != xno && test \"x$CONFIG_SHELL\" != x; then\n    _as_can_reexec=no; export _as_can_reexec;\n    # We cannot yet assume a decent shell, so we have to provide a\n# neutralization value for shells without unset; and this also\n# works around shells that cannot unset nonexistent variables.\n# Preserve -v and -x to the replacement shell.\nBASH_ENV=/dev/null\nENV=/dev/null\n(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV\ncase $- in # ((((\n  *v*x* | *x*v* ) as_opts=-vx ;;\n  *v* ) as_opts=-v ;;\n  *x* ) as_opts=-x ;;\n  * ) as_opts= ;;\nesac\nexec $CONFIG_SHELL $as_opts \"$as_myself\" ${1+\"$@\"}\n# Admittedly, this is quite paranoid, since all the known shells bail\n# out after a failed `exec'.\n$as_echo \"$0: could not re-execute with $CONFIG_SHELL\" >&2\nas_fn_exit 255\n  fi\n  # We don't want this to propagate to other subprocesses.\n          { _as_can_reexec=; unset _as_can_reexec;}\nif test \"x$CONFIG_SHELL\" = x; then\n  as_bourne_compatible=\"if test -n \\\"\\${ZSH_VERSION+set}\\\" && (emulate sh) >/dev/null 2>&1; then :\n  emulate sh\n  NULLCMD=:\n  # Pre-4.2 versions of Zsh do word splitting on \\${1+\\\"\\$@\\\"}, which\n  # is contrary to our usage.  Disable this feature.\n  alias -g '\\${1+\\\"\\$@\\\"}'='\\\"\\$@\\\"'\n  setopt NO_GLOB_SUBST\nelse\n  case \\`(set -o) 2>/dev/null\\` in #(\n  *posix*) :\n    set -o posix ;; #(\n  *) :\n     ;;\nesac\nfi\n\"\n  as_required=\"as_fn_return () { (exit \\$1); }\nas_fn_success () { as_fn_return 0; }\nas_fn_failure () { as_fn_return 1; }\nas_fn_ret_success () { return 0; }\nas_fn_ret_failure () { return 1; }\n\nexitcode=0\nas_fn_success || { exitcode=1; echo as_fn_success failed.; }\nas_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; }\nas_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; }\nas_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; }\nif ( set x; as_fn_ret_success y && test x = \\\"\\$1\\\" ); then :\n\nelse\n  exitcode=1; echo positional parameters were not saved.\nfi\ntest x\\$exitcode = x0 || exit 1\ntest -x / || exit 1\"\n  as_suggested=\"  as_lineno_1=\";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested\" as_lineno_1a=\\$LINENO\n  as_lineno_2=\";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested\" as_lineno_2a=\\$LINENO\n  eval 'test \\\"x\\$as_lineno_1'\\$as_run'\\\" != \\\"x\\$as_lineno_2'\\$as_run'\\\" &&\n  test \\\"x\\`expr \\$as_lineno_1'\\$as_run' + 1\\`\\\" = \\\"x\\$as_lineno_2'\\$as_run'\\\"' || exit 1\"\n  if (eval \"$as_required\") 2>/dev/null; then :\n  as_have_required=yes\nelse\n  as_have_required=no\nfi\n  if test x$as_have_required = xyes && (eval \"$as_suggested\") 2>/dev/null; then :\n\nelse\n  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\nas_found=false\nfor as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH\ndo\n  IFS=$as_save_IFS\n  test -z \"$as_dir\" && as_dir=.\n  as_found=:\n  case $as_dir in #(\n\t /*)\n\t   for as_base in sh bash ksh sh5; do\n\t     # Try only shells that exist, to save several forks.\n\t     as_shell=$as_dir/$as_base\n\t     if { test -f \"$as_shell\" || test -f \"$as_shell.exe\"; } &&\n\t\t    { $as_echo \"$as_bourne_compatible\"\"$as_required\" | as_run=a \"$as_shell\"; } 2>/dev/null; then :\n  CONFIG_SHELL=$as_shell as_have_required=yes\n\t\t   if { $as_echo \"$as_bourne_compatible\"\"$as_suggested\" | as_run=a \"$as_shell\"; } 2>/dev/null; then :\n  break 2\nfi\nfi\n\t   done;;\n       esac\n  as_found=false\ndone\n$as_found || { if { test -f \"$SHELL\" || test -f \"$SHELL.exe\"; } &&\n\t      { $as_echo \"$as_bourne_compatible\"\"$as_required\" | as_run=a \"$SHELL\"; } 2>/dev/null; then :\n  CONFIG_SHELL=$SHELL as_have_required=yes\nfi; }\nIFS=$as_save_IFS\n\n\n      if test \"x$CONFIG_SHELL\" != x; then :\n  export CONFIG_SHELL\n             # We cannot yet assume a decent shell, so we have to provide a\n# neutralization value for shells without unset; and this also\n# works around shells that cannot unset nonexistent variables.\n# Preserve -v and -x to the replacement shell.\nBASH_ENV=/dev/null\nENV=/dev/null\n(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV\ncase $- in # ((((\n  *v*x* | *x*v* ) as_opts=-vx ;;\n  *v* ) as_opts=-v ;;\n  *x* ) as_opts=-x ;;\n  * ) as_opts= ;;\nesac\nexec $CONFIG_SHELL $as_opts \"$as_myself\" ${1+\"$@\"}\n# Admittedly, this is quite paranoid, since all the known shells bail\n# out after a failed `exec'.\n$as_echo \"$0: could not re-execute with $CONFIG_SHELL\" >&2\nexit 255\nfi\n\n    if test x$as_have_required = xno; then :\n  $as_echo \"$0: This script requires a shell more modern than all\"\n  $as_echo \"$0: the shells that I found on your system.\"\n  if test x${ZSH_VERSION+set} = xset ; then\n    $as_echo \"$0: In particular, zsh $ZSH_VERSION has bugs and should\"\n    $as_echo \"$0: be upgraded to zsh 4.3.4 or later.\"\n  else\n    $as_echo \"$0: Please tell bug-autoconf@gnu.org and\n$0: https://github.com/abraunegg/onedrive about your\n$0: system, including any error possibly output before this\n$0: message. Then install a modern shell, or manually run\n$0: the script under such a shell if you do have one.\"\n  fi\n  exit 1\nfi\nfi\nfi\nSHELL=${CONFIG_SHELL-/bin/sh}\nexport SHELL\n# Unset more variables known to interfere with behavior of common tools.\nCLICOLOR_FORCE= GREP_OPTIONS=\nunset CLICOLOR_FORCE GREP_OPTIONS\n\n## --------------------- ##\n## M4sh Shell Functions. ##\n## --------------------- ##\n# as_fn_unset VAR\n# ---------------\n# Portably unset VAR.\nas_fn_unset ()\n{\n  { eval $1=; unset $1;}\n}\nas_unset=as_fn_unset\n\n# as_fn_set_status STATUS\n# -----------------------\n# Set $? to STATUS, without forking.\nas_fn_set_status ()\n{\n  return $1\n} # as_fn_set_status\n\n# as_fn_exit STATUS\n# -----------------\n# Exit the shell with STATUS, even in a \"trap 0\" or \"set -e\" context.\nas_fn_exit ()\n{\n  set +e\n  as_fn_set_status $1\n  exit $1\n} # as_fn_exit\n\n# as_fn_mkdir_p\n# -------------\n# Create \"$as_dir\" as a directory, including parents if necessary.\nas_fn_mkdir_p ()\n{\n\n  case $as_dir in #(\n  -*) as_dir=./$as_dir;;\n  esac\n  test -d \"$as_dir\" || eval $as_mkdir_p || {\n    as_dirs=\n    while :; do\n      case $as_dir in #(\n      *\\'*) as_qdir=`$as_echo \"$as_dir\" | sed \"s/'/'\\\\\\\\\\\\\\\\''/g\"`;; #'(\n      *) as_qdir=$as_dir;;\n      esac\n      as_dirs=\"'$as_qdir' $as_dirs\"\n      as_dir=`$as_dirname -- \"$as_dir\" ||\n$as_expr X\"$as_dir\" : 'X\\(.*[^/]\\)//*[^/][^/]*/*$' \\| \\\n\t X\"$as_dir\" : 'X\\(//\\)[^/]' \\| \\\n\t X\"$as_dir\" : 'X\\(//\\)$' \\| \\\n\t X\"$as_dir\" : 'X\\(/\\)' \\| . 2>/dev/null ||\n$as_echo X\"$as_dir\" |\n    sed '/^X\\(.*[^/]\\)\\/\\/*[^/][^/]*\\/*$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\/\\)[^/].*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\/\\)$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\).*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  s/.*/./; q'`\n      test -d \"$as_dir\" && break\n    done\n    test -z \"$as_dirs\" || eval \"mkdir $as_dirs\"\n  } || test -d \"$as_dir\" || as_fn_error $? \"cannot create directory $as_dir\"\n\n\n} # as_fn_mkdir_p\n\n# as_fn_executable_p FILE\n# -----------------------\n# Test if FILE is an executable regular file.\nas_fn_executable_p ()\n{\n  test -f \"$1\" && test -x \"$1\"\n} # as_fn_executable_p\n# as_fn_append VAR VALUE\n# ----------------------\n# Append the text in VALUE to the end of the definition contained in VAR. Take\n# advantage of any shell optimizations that allow amortized linear growth over\n# repeated appends, instead of the typical quadratic growth present in naive\n# implementations.\nif (eval \"as_var=1; as_var+=2; test x\\$as_var = x12\") 2>/dev/null; then :\n  eval 'as_fn_append ()\n  {\n    eval $1+=\\$2\n  }'\nelse\n  as_fn_append ()\n  {\n    eval $1=\\$$1\\$2\n  }\nfi # as_fn_append\n\n# as_fn_arith ARG...\n# ------------------\n# Perform arithmetic evaluation on the ARGs, and store the result in the\n# global $as_val. Take advantage of shells that can avoid forks. The arguments\n# must be portable across $(()) and expr.\nif (eval \"test \\$(( 1 + 1 )) = 2\") 2>/dev/null; then :\n  eval 'as_fn_arith ()\n  {\n    as_val=$(( $* ))\n  }'\nelse\n  as_fn_arith ()\n  {\n    as_val=`expr \"$@\" || test $? -eq 1`\n  }\nfi # as_fn_arith\n\n\n# as_fn_error STATUS ERROR [LINENO LOG_FD]\n# ----------------------------------------\n# Output \"`basename $0`: error: ERROR\" to stderr. If LINENO and LOG_FD are\n# provided, also output the error to LOG_FD, referencing LINENO. Then exit the\n# script with STATUS, using 1 if that was 0.\nas_fn_error ()\n{\n  as_status=$1; test $as_status -eq 0 && as_status=1\n  if test \"$4\"; then\n    as_lineno=${as_lineno-\"$3\"} as_lineno_stack=as_lineno_stack=$as_lineno_stack\n    $as_echo \"$as_me:${as_lineno-$LINENO}: error: $2\" >&$4\n  fi\n  $as_echo \"$as_me: error: $2\" >&2\n  as_fn_exit $as_status\n} # as_fn_error\n\nif expr a : '\\(a\\)' >/dev/null 2>&1 &&\n   test \"X`expr 00001 : '.*\\(...\\)'`\" = X001; then\n  as_expr=expr\nelse\n  as_expr=false\nfi\n\nif (basename -- /) >/dev/null 2>&1 && test \"X`basename -- / 2>&1`\" = \"X/\"; then\n  as_basename=basename\nelse\n  as_basename=false\nfi\n\nif (as_dir=`dirname -- /` && test \"X$as_dir\" = X/) >/dev/null 2>&1; then\n  as_dirname=dirname\nelse\n  as_dirname=false\nfi\n\nas_me=`$as_basename -- \"$0\" ||\n$as_expr X/\"$0\" : '.*/\\([^/][^/]*\\)/*$' \\| \\\n\t X\"$0\" : 'X\\(//\\)$' \\| \\\n\t X\"$0\" : 'X\\(/\\)' \\| . 2>/dev/null ||\n$as_echo X/\"$0\" |\n    sed '/^.*\\/\\([^/][^/]*\\)\\/*$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\/\\(\\/\\/\\)$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\/\\(\\/\\).*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  s/.*/./; q'`\n\n# Avoid depending upon Character Ranges.\nas_cr_letters='abcdefghijklmnopqrstuvwxyz'\nas_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ'\nas_cr_Letters=$as_cr_letters$as_cr_LETTERS\nas_cr_digits='0123456789'\nas_cr_alnum=$as_cr_Letters$as_cr_digits\n\n\n  as_lineno_1=$LINENO as_lineno_1a=$LINENO\n  as_lineno_2=$LINENO as_lineno_2a=$LINENO\n  eval 'test \"x$as_lineno_1'$as_run'\" != \"x$as_lineno_2'$as_run'\" &&\n  test \"x`expr $as_lineno_1'$as_run' + 1`\" = \"x$as_lineno_2'$as_run'\"' || {\n  # Blame Lee E. McMahon (1931-1989) for sed's syntax.  :-)\n  sed -n '\n    p\n    /[$]LINENO/=\n  ' <$as_myself |\n    sed '\n      s/[$]LINENO.*/&-/\n      t lineno\n      b\n      :lineno\n      N\n      :loop\n      s/[$]LINENO\\([^'$as_cr_alnum'_].*\\n\\)\\(.*\\)/\\2\\1\\2/\n      t loop\n      s/-\\n.*//\n    ' >$as_me.lineno &&\n  chmod +x \"$as_me.lineno\" ||\n    { $as_echo \"$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell\" >&2; as_fn_exit 1; }\n\n  # If we had to re-execute with $CONFIG_SHELL, we're ensured to have\n  # already done that, so ensure we don't try to do so again and fall\n  # in an infinite loop.  This has already happened in practice.\n  _as_can_reexec=no; export _as_can_reexec\n  # Don't try to exec as it changes $[0], causing all sort of problems\n  # (the dirname of $[0] is not the place where we might find the\n  # original and so on.  Autoconf is especially sensitive to this).\n  . \"./$as_me.lineno\"\n  # Exit status is that of the last command.\n  exit\n}\n\nECHO_C= ECHO_N= ECHO_T=\ncase `echo -n x` in #(((((\n-n*)\n  case `echo 'xy\\c'` in\n  *c*) ECHO_T='\t';;\t# ECHO_T is single tab character.\n  xy)  ECHO_C='\\c';;\n  *)   echo `echo ksh88 bug on AIX 6.1` > /dev/null\n       ECHO_T='\t';;\n  esac;;\n*)\n  ECHO_N='-n';;\nesac\n\nrm -f conf$$ conf$$.exe conf$$.file\nif test -d conf$$.dir; then\n  rm -f conf$$.dir/conf$$.file\nelse\n  rm -f conf$$.dir\n  mkdir conf$$.dir 2>/dev/null\nfi\nif (echo >conf$$.file) 2>/dev/null; then\n  if ln -s conf$$.file conf$$ 2>/dev/null; then\n    as_ln_s='ln -s'\n    # ... but there are two gotchas:\n    # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail.\n    # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable.\n    # In both cases, we have to default to `cp -pR'.\n    ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe ||\n      as_ln_s='cp -pR'\n  elif ln conf$$.file conf$$ 2>/dev/null; then\n    as_ln_s=ln\n  else\n    as_ln_s='cp -pR'\n  fi\nelse\n  as_ln_s='cp -pR'\nfi\nrm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file\nrmdir conf$$.dir 2>/dev/null\n\nif mkdir -p . 2>/dev/null; then\n  as_mkdir_p='mkdir -p \"$as_dir\"'\nelse\n  test -d ./-p && rmdir ./-p\n  as_mkdir_p=false\nfi\n\nas_test_x='test -x'\nas_executable_p=as_fn_executable_p\n\n# Sed expression to map a string onto a valid CPP name.\nas_tr_cpp=\"eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'\"\n\n# Sed expression to map a string onto a valid variable name.\nas_tr_sh=\"eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'\"\n\n\ntest -n \"$DJDIR\" || exec 7<&0 </dev/null\nexec 6>&1\n\n# Name of the host.\n# hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status,\n# so uname gets run too.\nac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q`\n\n#\n# Initializations.\n#\nac_default_prefix=/usr/local\nac_clean_files=\nac_config_libobj_dir=.\nLIBOBJS=\ncross_compiling=no\nsubdirs=\nMFLAGS=\nMAKEFLAGS=\n\n# Identity of this package.\nPACKAGE_NAME='onedrive'\nPACKAGE_TARNAME='onedrive'\nPACKAGE_VERSION='v2.5.10'\nPACKAGE_STRING='onedrive v2.5.10'\nPACKAGE_BUGREPORT='https://github.com/abraunegg/onedrive'\nPACKAGE_URL=''\n\nac_unique_file=\"src/main.d\"\nac_subst_vars='LTLIBOBJS\nLIBOBJS\nDEBUG\nFISH_COMPLETION_DIR\nZSH_COMPLETION_DIR\nBASH_COMPLETION_DIR\nbashcompdir\nCOMPLETIONS\ndynamic_linker_LIBS\nbsd_inotify_LIBS\nNOTIFICATIONS\nnotify_LIBS\nnotify_CFLAGS\nHAVE_SYSTEMD\nsystemduserunitdir\nsystemdsystemunitdir\nenable_dbus\ndbus_LIBS\ndbus_CFLAGS\nsqlite_LIBS\nsqlite_CFLAGS\ncurl_LIBS\ncurl_CFLAGS\nWERROR_DCFLAG\nOUTPUT_DCFLAG\nLINKER_DCFLAG\nVERSION_DCFLAG\nRELEASE_DCFLAGS\nDEBUG_DCFLAGS\nPACKAGE_DATE\nPKG_CONFIG_LIBDIR\nPKG_CONFIG_PATH\nPKG_CONFIG\nINSTALL_DATA\nINSTALL_SCRIPT\nINSTALL_PROGRAM\nDCFLAGS\nDC\ntarget_alias\nhost_alias\nbuild_alias\nLIBS\nECHO_T\nECHO_N\nECHO_C\nDEFS\nmandir\nlocaledir\nlibdir\npsdir\npdfdir\ndvidir\nhtmldir\ninfodir\ndocdir\noldincludedir\nincludedir\nlocalstatedir\nsharedstatedir\nsysconfdir\ndatadir\ndatarootdir\nlibexecdir\nsbindir\nbindir\nprogram_transform_name\nprefix\nexec_prefix\nPACKAGE_URL\nPACKAGE_BUGREPORT\nPACKAGE_STRING\nPACKAGE_VERSION\nPACKAGE_TARNAME\nPACKAGE_NAME\nPATH_SEPARATOR\nSHELL'\nac_subst_files=''\nac_user_opts='\nenable_option_checking\nenable_version_check\nwith_systemdsystemunitdir\nwith_systemduserunitdir\nenable_notifications\nenable_completions\nwith_bash_completion_dir\nwith_zsh_completion_dir\nwith_fish_completion_dir\nenable_debug\n'\n      ac_precious_vars='build_alias\nhost_alias\ntarget_alias\nDC\nDCFLAGS\nPKG_CONFIG\nPKG_CONFIG_PATH\nPKG_CONFIG_LIBDIR\ncurl_CFLAGS\ncurl_LIBS\nsqlite_CFLAGS\nsqlite_LIBS\ndbus_CFLAGS\ndbus_LIBS\nnotify_CFLAGS\nnotify_LIBS\nbashcompdir'\n\n\n# Initialize some variables set by options.\nac_init_help=\nac_init_version=false\nac_unrecognized_opts=\nac_unrecognized_sep=\n# The variables have the same names as the options, with\n# dashes changed to underlines.\ncache_file=/dev/null\nexec_prefix=NONE\nno_create=\nno_recursion=\nprefix=NONE\nprogram_prefix=NONE\nprogram_suffix=NONE\nprogram_transform_name=s,x,x,\nsilent=\nsite=\nsrcdir=\nverbose=\nx_includes=NONE\nx_libraries=NONE\n\n# Installation directory options.\n# These are left unexpanded so users can \"make install exec_prefix=/foo\"\n# and all the variables that are supposed to be based on exec_prefix\n# by default will actually change.\n# Use braces instead of parens because sh, perl, etc. also accept them.\n# (The list follows the same order as the GNU Coding Standards.)\nbindir='${exec_prefix}/bin'\nsbindir='${exec_prefix}/sbin'\nlibexecdir='${exec_prefix}/libexec'\ndatarootdir='${prefix}/share'\ndatadir='${datarootdir}'\nsysconfdir='${prefix}/etc'\nsharedstatedir='${prefix}/com'\nlocalstatedir='${prefix}/var'\nincludedir='${prefix}/include'\noldincludedir='/usr/include'\ndocdir='${datarootdir}/doc/${PACKAGE_TARNAME}'\ninfodir='${datarootdir}/info'\nhtmldir='${docdir}'\ndvidir='${docdir}'\npdfdir='${docdir}'\npsdir='${docdir}'\nlibdir='${exec_prefix}/lib'\nlocaledir='${datarootdir}/locale'\nmandir='${datarootdir}/man'\n\nac_prev=\nac_dashdash=\nfor ac_option\ndo\n  # If the previous option needs an argument, assign it.\n  if test -n \"$ac_prev\"; then\n    eval $ac_prev=\\$ac_option\n    ac_prev=\n    continue\n  fi\n\n  case $ac_option in\n  *=?*) ac_optarg=`expr \"X$ac_option\" : '[^=]*=\\(.*\\)'` ;;\n  *=)   ac_optarg= ;;\n  *)    ac_optarg=yes ;;\n  esac\n\n  # Accept the important Cygnus configure options, so we can diagnose typos.\n\n  case $ac_dashdash$ac_option in\n  --)\n    ac_dashdash=yes ;;\n\n  -bindir | --bindir | --bindi | --bind | --bin | --bi)\n    ac_prev=bindir ;;\n  -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*)\n    bindir=$ac_optarg ;;\n\n  -build | --build | --buil | --bui | --bu)\n    ac_prev=build_alias ;;\n  -build=* | --build=* | --buil=* | --bui=* | --bu=*)\n    build_alias=$ac_optarg ;;\n\n  -cache-file | --cache-file | --cache-fil | --cache-fi \\\n  | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c)\n    ac_prev=cache_file ;;\n  -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \\\n  | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*)\n    cache_file=$ac_optarg ;;\n\n  --config-cache | -C)\n    cache_file=config.cache ;;\n\n  -datadir | --datadir | --datadi | --datad)\n    ac_prev=datadir ;;\n  -datadir=* | --datadir=* | --datadi=* | --datad=*)\n    datadir=$ac_optarg ;;\n\n  -datarootdir | --datarootdir | --datarootdi | --datarootd | --dataroot \\\n  | --dataroo | --dataro | --datar)\n    ac_prev=datarootdir ;;\n  -datarootdir=* | --datarootdir=* | --datarootdi=* | --datarootd=* \\\n  | --dataroot=* | --dataroo=* | --dataro=* | --datar=*)\n    datarootdir=$ac_optarg ;;\n\n  -disable-* | --disable-*)\n    ac_useropt=`expr \"x$ac_option\" : 'x-*disable-\\(.*\\)'`\n    # Reject names that are not valid shell variable names.\n    expr \"x$ac_useropt\" : \".*[^-+._$as_cr_alnum]\" >/dev/null &&\n      as_fn_error $? \"invalid feature name: $ac_useropt\"\n    ac_useropt_orig=$ac_useropt\n    ac_useropt=`$as_echo \"$ac_useropt\" | sed 's/[-+.]/_/g'`\n    case $ac_user_opts in\n      *\"\n\"enable_$ac_useropt\"\n\"*) ;;\n      *) ac_unrecognized_opts=\"$ac_unrecognized_opts$ac_unrecognized_sep--disable-$ac_useropt_orig\"\n\t ac_unrecognized_sep=', ';;\n    esac\n    eval enable_$ac_useropt=no ;;\n\n  -docdir | --docdir | --docdi | --doc | --do)\n    ac_prev=docdir ;;\n  -docdir=* | --docdir=* | --docdi=* | --doc=* | --do=*)\n    docdir=$ac_optarg ;;\n\n  -dvidir | --dvidir | --dvidi | --dvid | --dvi | --dv)\n    ac_prev=dvidir ;;\n  -dvidir=* | --dvidir=* | --dvidi=* | --dvid=* | --dvi=* | --dv=*)\n    dvidir=$ac_optarg ;;\n\n  -enable-* | --enable-*)\n    ac_useropt=`expr \"x$ac_option\" : 'x-*enable-\\([^=]*\\)'`\n    # Reject names that are not valid shell variable names.\n    expr \"x$ac_useropt\" : \".*[^-+._$as_cr_alnum]\" >/dev/null &&\n      as_fn_error $? \"invalid feature name: $ac_useropt\"\n    ac_useropt_orig=$ac_useropt\n    ac_useropt=`$as_echo \"$ac_useropt\" | sed 's/[-+.]/_/g'`\n    case $ac_user_opts in\n      *\"\n\"enable_$ac_useropt\"\n\"*) ;;\n      *) ac_unrecognized_opts=\"$ac_unrecognized_opts$ac_unrecognized_sep--enable-$ac_useropt_orig\"\n\t ac_unrecognized_sep=', ';;\n    esac\n    eval enable_$ac_useropt=\\$ac_optarg ;;\n\n  -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \\\n  | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \\\n  | --exec | --exe | --ex)\n    ac_prev=exec_prefix ;;\n  -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \\\n  | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \\\n  | --exec=* | --exe=* | --ex=*)\n    exec_prefix=$ac_optarg ;;\n\n  -gas | --gas | --ga | --g)\n    # Obsolete; use --with-gas.\n    with_gas=yes ;;\n\n  -help | --help | --hel | --he | -h)\n    ac_init_help=long ;;\n  -help=r* | --help=r* | --hel=r* | --he=r* | -hr*)\n    ac_init_help=recursive ;;\n  -help=s* | --help=s* | --hel=s* | --he=s* | -hs*)\n    ac_init_help=short ;;\n\n  -host | --host | --hos | --ho)\n    ac_prev=host_alias ;;\n  -host=* | --host=* | --hos=* | --ho=*)\n    host_alias=$ac_optarg ;;\n\n  -htmldir | --htmldir | --htmldi | --htmld | --html | --htm | --ht)\n    ac_prev=htmldir ;;\n  -htmldir=* | --htmldir=* | --htmldi=* | --htmld=* | --html=* | --htm=* \\\n  | --ht=*)\n    htmldir=$ac_optarg ;;\n\n  -includedir | --includedir | --includedi | --included | --include \\\n  | --includ | --inclu | --incl | --inc)\n    ac_prev=includedir ;;\n  -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \\\n  | --includ=* | --inclu=* | --incl=* | --inc=*)\n    includedir=$ac_optarg ;;\n\n  -infodir | --infodir | --infodi | --infod | --info | --inf)\n    ac_prev=infodir ;;\n  -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*)\n    infodir=$ac_optarg ;;\n\n  -libdir | --libdir | --libdi | --libd)\n    ac_prev=libdir ;;\n  -libdir=* | --libdir=* | --libdi=* | --libd=*)\n    libdir=$ac_optarg ;;\n\n  -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \\\n  | --libexe | --libex | --libe)\n    ac_prev=libexecdir ;;\n  -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \\\n  | --libexe=* | --libex=* | --libe=*)\n    libexecdir=$ac_optarg ;;\n\n  -localedir | --localedir | --localedi | --localed | --locale)\n    ac_prev=localedir ;;\n  -localedir=* | --localedir=* | --localedi=* | --localed=* | --locale=*)\n    localedir=$ac_optarg ;;\n\n  -localstatedir | --localstatedir | --localstatedi | --localstated \\\n  | --localstate | --localstat | --localsta | --localst | --locals)\n    ac_prev=localstatedir ;;\n  -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \\\n  | --localstate=* | --localstat=* | --localsta=* | --localst=* | --locals=*)\n    localstatedir=$ac_optarg ;;\n\n  -mandir | --mandir | --mandi | --mand | --man | --ma | --m)\n    ac_prev=mandir ;;\n  -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*)\n    mandir=$ac_optarg ;;\n\n  -nfp | --nfp | --nf)\n    # Obsolete; use --without-fp.\n    with_fp=no ;;\n\n  -no-create | --no-create | --no-creat | --no-crea | --no-cre \\\n  | --no-cr | --no-c | -n)\n    no_create=yes ;;\n\n  -no-recursion | --no-recursion | --no-recursio | --no-recursi \\\n  | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r)\n    no_recursion=yes ;;\n\n  -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \\\n  | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \\\n  | --oldin | --oldi | --old | --ol | --o)\n    ac_prev=oldincludedir ;;\n  -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \\\n  | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \\\n  | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*)\n    oldincludedir=$ac_optarg ;;\n\n  -prefix | --prefix | --prefi | --pref | --pre | --pr | --p)\n    ac_prev=prefix ;;\n  -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*)\n    prefix=$ac_optarg ;;\n\n  -program-prefix | --program-prefix | --program-prefi | --program-pref \\\n  | --program-pre | --program-pr | --program-p)\n    ac_prev=program_prefix ;;\n  -program-prefix=* | --program-prefix=* | --program-prefi=* \\\n  | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*)\n    program_prefix=$ac_optarg ;;\n\n  -program-suffix | --program-suffix | --program-suffi | --program-suff \\\n  | --program-suf | --program-su | --program-s)\n    ac_prev=program_suffix ;;\n  -program-suffix=* | --program-suffix=* | --program-suffi=* \\\n  | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*)\n    program_suffix=$ac_optarg ;;\n\n  -program-transform-name | --program-transform-name \\\n  | --program-transform-nam | --program-transform-na \\\n  | --program-transform-n | --program-transform- \\\n  | --program-transform | --program-transfor \\\n  | --program-transfo | --program-transf \\\n  | --program-trans | --program-tran \\\n  | --progr-tra | --program-tr | --program-t)\n    ac_prev=program_transform_name ;;\n  -program-transform-name=* | --program-transform-name=* \\\n  | --program-transform-nam=* | --program-transform-na=* \\\n  | --program-transform-n=* | --program-transform-=* \\\n  | --program-transform=* | --program-transfor=* \\\n  | --program-transfo=* | --program-transf=* \\\n  | --program-trans=* | --program-tran=* \\\n  | --progr-tra=* | --program-tr=* | --program-t=*)\n    program_transform_name=$ac_optarg ;;\n\n  -pdfdir | --pdfdir | --pdfdi | --pdfd | --pdf | --pd)\n    ac_prev=pdfdir ;;\n  -pdfdir=* | --pdfdir=* | --pdfdi=* | --pdfd=* | --pdf=* | --pd=*)\n    pdfdir=$ac_optarg ;;\n\n  -psdir | --psdir | --psdi | --psd | --ps)\n    ac_prev=psdir ;;\n  -psdir=* | --psdir=* | --psdi=* | --psd=* | --ps=*)\n    psdir=$ac_optarg ;;\n\n  -q | -quiet | --quiet | --quie | --qui | --qu | --q \\\n  | -silent | --silent | --silen | --sile | --sil)\n    silent=yes ;;\n\n  -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb)\n    ac_prev=sbindir ;;\n  -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \\\n  | --sbi=* | --sb=*)\n    sbindir=$ac_optarg ;;\n\n  -sharedstatedir | --sharedstatedir | --sharedstatedi \\\n  | --sharedstated | --sharedstate | --sharedstat | --sharedsta \\\n  | --sharedst | --shareds | --shared | --share | --shar \\\n  | --sha | --sh)\n    ac_prev=sharedstatedir ;;\n  -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \\\n  | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \\\n  | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \\\n  | --sha=* | --sh=*)\n    sharedstatedir=$ac_optarg ;;\n\n  -site | --site | --sit)\n    ac_prev=site ;;\n  -site=* | --site=* | --sit=*)\n    site=$ac_optarg ;;\n\n  -srcdir | --srcdir | --srcdi | --srcd | --src | --sr)\n    ac_prev=srcdir ;;\n  -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*)\n    srcdir=$ac_optarg ;;\n\n  -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \\\n  | --syscon | --sysco | --sysc | --sys | --sy)\n    ac_prev=sysconfdir ;;\n  -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \\\n  | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*)\n    sysconfdir=$ac_optarg ;;\n\n  -target | --target | --targe | --targ | --tar | --ta | --t)\n    ac_prev=target_alias ;;\n  -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*)\n    target_alias=$ac_optarg ;;\n\n  -v | -verbose | --verbose | --verbos | --verbo | --verb)\n    verbose=yes ;;\n\n  -version | --version | --versio | --versi | --vers | -V)\n    ac_init_version=: ;;\n\n  -with-* | --with-*)\n    ac_useropt=`expr \"x$ac_option\" : 'x-*with-\\([^=]*\\)'`\n    # Reject names that are not valid shell variable names.\n    expr \"x$ac_useropt\" : \".*[^-+._$as_cr_alnum]\" >/dev/null &&\n      as_fn_error $? \"invalid package name: $ac_useropt\"\n    ac_useropt_orig=$ac_useropt\n    ac_useropt=`$as_echo \"$ac_useropt\" | sed 's/[-+.]/_/g'`\n    case $ac_user_opts in\n      *\"\n\"with_$ac_useropt\"\n\"*) ;;\n      *) ac_unrecognized_opts=\"$ac_unrecognized_opts$ac_unrecognized_sep--with-$ac_useropt_orig\"\n\t ac_unrecognized_sep=', ';;\n    esac\n    eval with_$ac_useropt=\\$ac_optarg ;;\n\n  -without-* | --without-*)\n    ac_useropt=`expr \"x$ac_option\" : 'x-*without-\\(.*\\)'`\n    # Reject names that are not valid shell variable names.\n    expr \"x$ac_useropt\" : \".*[^-+._$as_cr_alnum]\" >/dev/null &&\n      as_fn_error $? \"invalid package name: $ac_useropt\"\n    ac_useropt_orig=$ac_useropt\n    ac_useropt=`$as_echo \"$ac_useropt\" | sed 's/[-+.]/_/g'`\n    case $ac_user_opts in\n      *\"\n\"with_$ac_useropt\"\n\"*) ;;\n      *) ac_unrecognized_opts=\"$ac_unrecognized_opts$ac_unrecognized_sep--without-$ac_useropt_orig\"\n\t ac_unrecognized_sep=', ';;\n    esac\n    eval with_$ac_useropt=no ;;\n\n  --x)\n    # Obsolete; use --with-x.\n    with_x=yes ;;\n\n  -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \\\n  | --x-incl | --x-inc | --x-in | --x-i)\n    ac_prev=x_includes ;;\n  -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \\\n  | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*)\n    x_includes=$ac_optarg ;;\n\n  -x-libraries | --x-libraries | --x-librarie | --x-librari \\\n  | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l)\n    ac_prev=x_libraries ;;\n  -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \\\n  | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*)\n    x_libraries=$ac_optarg ;;\n\n  -*) as_fn_error $? \"unrecognized option: \\`$ac_option'\nTry \\`$0 --help' for more information\"\n    ;;\n\n  *=*)\n    ac_envvar=`expr \"x$ac_option\" : 'x\\([^=]*\\)='`\n    # Reject names that are not valid shell variable names.\n    case $ac_envvar in #(\n      '' | [0-9]* | *[!_$as_cr_alnum]* )\n      as_fn_error $? \"invalid variable name: \\`$ac_envvar'\" ;;\n    esac\n    eval $ac_envvar=\\$ac_optarg\n    export $ac_envvar ;;\n\n  *)\n    # FIXME: should be removed in autoconf 3.0.\n    $as_echo \"$as_me: WARNING: you should use --build, --host, --target\" >&2\n    expr \"x$ac_option\" : \".*[^-._$as_cr_alnum]\" >/dev/null &&\n      $as_echo \"$as_me: WARNING: invalid host type: $ac_option\" >&2\n    : \"${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}\"\n    ;;\n\n  esac\ndone\n\nif test -n \"$ac_prev\"; then\n  ac_option=--`echo $ac_prev | sed 's/_/-/g'`\n  as_fn_error $? \"missing argument to $ac_option\"\nfi\n\nif test -n \"$ac_unrecognized_opts\"; then\n  case $enable_option_checking in\n    no) ;;\n    fatal) as_fn_error $? \"unrecognized options: $ac_unrecognized_opts\" ;;\n    *)     $as_echo \"$as_me: WARNING: unrecognized options: $ac_unrecognized_opts\" >&2 ;;\n  esac\nfi\n\n# Check all directory arguments for consistency.\nfor ac_var in\texec_prefix prefix bindir sbindir libexecdir datarootdir \\\n\t\tdatadir sysconfdir sharedstatedir localstatedir includedir \\\n\t\toldincludedir docdir infodir htmldir dvidir pdfdir psdir \\\n\t\tlibdir localedir mandir\ndo\n  eval ac_val=\\$$ac_var\n  # Remove trailing slashes.\n  case $ac_val in\n    */ )\n      ac_val=`expr \"X$ac_val\" : 'X\\(.*[^/]\\)' \\| \"X$ac_val\" : 'X\\(.*\\)'`\n      eval $ac_var=\\$ac_val;;\n  esac\n  # Be sure to have absolute directory names.\n  case $ac_val in\n    [\\\\/$]* | ?:[\\\\/]* )  continue;;\n    NONE | '' ) case $ac_var in *prefix ) continue;; esac;;\n  esac\n  as_fn_error $? \"expected an absolute directory name for --$ac_var: $ac_val\"\ndone\n\n# There might be people who depend on the old broken behavior: `$host'\n# used to hold the argument of --host etc.\n# FIXME: To remove some day.\nbuild=$build_alias\nhost=$host_alias\ntarget=$target_alias\n\n# FIXME: To remove some day.\nif test \"x$host_alias\" != x; then\n  if test \"x$build_alias\" = x; then\n    cross_compiling=maybe\n  elif test \"x$build_alias\" != \"x$host_alias\"; then\n    cross_compiling=yes\n  fi\nfi\n\nac_tool_prefix=\ntest -n \"$host_alias\" && ac_tool_prefix=$host_alias-\n\ntest \"$silent\" = yes && exec 6>/dev/null\n\n\nac_pwd=`pwd` && test -n \"$ac_pwd\" &&\nac_ls_di=`ls -di .` &&\nac_pwd_ls_di=`cd \"$ac_pwd\" && ls -di .` ||\n  as_fn_error $? \"working directory cannot be determined\"\ntest \"X$ac_ls_di\" = \"X$ac_pwd_ls_di\" ||\n  as_fn_error $? \"pwd does not report name of working directory\"\n\n\n# Find the source files, if location was not specified.\nif test -z \"$srcdir\"; then\n  ac_srcdir_defaulted=yes\n  # Try the directory containing this script, then the parent directory.\n  ac_confdir=`$as_dirname -- \"$as_myself\" ||\n$as_expr X\"$as_myself\" : 'X\\(.*[^/]\\)//*[^/][^/]*/*$' \\| \\\n\t X\"$as_myself\" : 'X\\(//\\)[^/]' \\| \\\n\t X\"$as_myself\" : 'X\\(//\\)$' \\| \\\n\t X\"$as_myself\" : 'X\\(/\\)' \\| . 2>/dev/null ||\n$as_echo X\"$as_myself\" |\n    sed '/^X\\(.*[^/]\\)\\/\\/*[^/][^/]*\\/*$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\/\\)[^/].*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\/\\)$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\).*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  s/.*/./; q'`\n  srcdir=$ac_confdir\n  if test ! -r \"$srcdir/$ac_unique_file\"; then\n    srcdir=..\n  fi\nelse\n  ac_srcdir_defaulted=no\nfi\nif test ! -r \"$srcdir/$ac_unique_file\"; then\n  test \"$ac_srcdir_defaulted\" = yes && srcdir=\"$ac_confdir or ..\"\n  as_fn_error $? \"cannot find sources ($ac_unique_file) in $srcdir\"\nfi\nac_msg=\"sources are in $srcdir, but \\`cd $srcdir' does not work\"\nac_abs_confdir=`(\n\tcd \"$srcdir\" && test -r \"./$ac_unique_file\" || as_fn_error $? \"$ac_msg\"\n\tpwd)`\n# When building in place, set srcdir=.\nif test \"$ac_abs_confdir\" = \"$ac_pwd\"; then\n  srcdir=.\nfi\n# Remove unnecessary trailing slashes from srcdir.\n# Double slashes in file names in object file debugging info\n# mess up M-x gdb in Emacs.\ncase $srcdir in\n*/) srcdir=`expr \"X$srcdir\" : 'X\\(.*[^/]\\)' \\| \"X$srcdir\" : 'X\\(.*\\)'`;;\nesac\nfor ac_var in $ac_precious_vars; do\n  eval ac_env_${ac_var}_set=\\${${ac_var}+set}\n  eval ac_env_${ac_var}_value=\\$${ac_var}\n  eval ac_cv_env_${ac_var}_set=\\${${ac_var}+set}\n  eval ac_cv_env_${ac_var}_value=\\$${ac_var}\ndone\n\n#\n# Report the --help message.\n#\nif test \"$ac_init_help\" = \"long\"; then\n  # Omit some internal or obsolete options to make the list less imposing.\n  # This message is too long to be a string in the A/UX 3.1 sh.\n  cat <<_ACEOF\n\\`configure' configures onedrive v2.5.10 to adapt to many kinds of systems.\n\nUsage: $0 [OPTION]... [VAR=VALUE]...\n\nTo assign environment variables (e.g., CC, CFLAGS...), specify them as\nVAR=VALUE.  See below for descriptions of some of the useful variables.\n\nDefaults for the options are specified in brackets.\n\nConfiguration:\n  -h, --help              display this help and exit\n      --help=short        display options specific to this package\n      --help=recursive    display the short help of all the included packages\n  -V, --version           display version information and exit\n  -q, --quiet, --silent   do not print \\`checking ...' messages\n      --cache-file=FILE   cache test results in FILE [disabled]\n  -C, --config-cache      alias for \\`--cache-file=config.cache'\n  -n, --no-create         do not create output files\n      --srcdir=DIR        find the sources in DIR [configure dir or \\`..']\n\nInstallation directories:\n  --prefix=PREFIX         install architecture-independent files in PREFIX\n                          [$ac_default_prefix]\n  --exec-prefix=EPREFIX   install architecture-dependent files in EPREFIX\n                          [PREFIX]\n\nBy default, \\`make install' will install all the files in\n\\`$ac_default_prefix/bin', \\`$ac_default_prefix/lib' etc.  You can specify\nan installation prefix other than \\`$ac_default_prefix' using \\`--prefix',\nfor instance \\`--prefix=\\$HOME'.\n\nFor better control, use the options below.\n\nFine tuning of the installation directories:\n  --bindir=DIR            user executables [EPREFIX/bin]\n  --sbindir=DIR           system admin executables [EPREFIX/sbin]\n  --libexecdir=DIR        program executables [EPREFIX/libexec]\n  --sysconfdir=DIR        read-only single-machine data [PREFIX/etc]\n  --sharedstatedir=DIR    modifiable architecture-independent data [PREFIX/com]\n  --localstatedir=DIR     modifiable single-machine data [PREFIX/var]\n  --libdir=DIR            object code libraries [EPREFIX/lib]\n  --includedir=DIR        C header files [PREFIX/include]\n  --oldincludedir=DIR     C header files for non-gcc [/usr/include]\n  --datarootdir=DIR       read-only arch.-independent data root [PREFIX/share]\n  --datadir=DIR           read-only architecture-independent data [DATAROOTDIR]\n  --infodir=DIR           info documentation [DATAROOTDIR/info]\n  --localedir=DIR         locale-dependent data [DATAROOTDIR/locale]\n  --mandir=DIR            man documentation [DATAROOTDIR/man]\n  --docdir=DIR            documentation root [DATAROOTDIR/doc/onedrive]\n  --htmldir=DIR           html documentation [DOCDIR]\n  --dvidir=DIR            dvi documentation [DOCDIR]\n  --pdfdir=DIR            pdf documentation [DOCDIR]\n  --psdir=DIR             ps documentation [DOCDIR]\n_ACEOF\n\n  cat <<\\_ACEOF\n_ACEOF\nfi\n\nif test -n \"$ac_init_help\"; then\n  case $ac_init_help in\n     short | recursive ) echo \"Configuration of onedrive v2.5.10:\";;\n   esac\n  cat <<\\_ACEOF\n\nOptional Features:\n  --disable-option-checking  ignore unrecognized --enable/--with options\n  --disable-FEATURE       do not include FEATURE (same as --enable-FEATURE=no)\n  --enable-FEATURE[=ARG]  include FEATURE [ARG=yes]\n  --disable-version-check Disable checks of compiler version during configure\n                          time\n  --enable-notifications  Enable desktop notifications via libnotify\n  --enable-completions    Install shell completions for bash, zsh, and fish\n  --enable-debug          Pass debug option to the compiler\n\nOptional Packages:\n  --with-PACKAGE[=ARG]    use PACKAGE [ARG=yes]\n  --without-PACKAGE       do not use PACKAGE (same as --with-PACKAGE=no)\n  --with-systemdsystemunitdir=DIR\n                          Directory for systemd system service files\n  --with-systemduserunitdir=DIR\n                          Directory for systemd user service files\n  --with-bash-completion-dir=DIR\n                          Directory for bash completion files\n  --with-zsh-completion-dir=DIR\n                          Directory for zsh completion files\n  --with-fish-completion-dir=DIR\n                          Directory for fish completion files\n\nSome influential environment variables:\n  DC          D compiler executable\n  DCFLAGS     flags for D compiler\n  PKG_CONFIG  path to pkg-config utility\n  PKG_CONFIG_PATH\n              directories to add to pkg-config's search path\n  PKG_CONFIG_LIBDIR\n              path overriding pkg-config's built-in search path\n  curl_CFLAGS C compiler flags for curl, overriding pkg-config\n  curl_LIBS   linker flags for curl, overriding pkg-config\n  sqlite_CFLAGS\n              C compiler flags for sqlite, overriding pkg-config\n  sqlite_LIBS linker flags for sqlite, overriding pkg-config\n  dbus_CFLAGS C compiler flags for dbus, overriding pkg-config\n  dbus_LIBS   linker flags for dbus, overriding pkg-config\n  notify_CFLAGS\n              C compiler flags for notify, overriding pkg-config\n  notify_LIBS linker flags for notify, overriding pkg-config\n  bashcompdir value of completionsdir for bash-completion, overriding\n              pkg-config\n\nUse these variables to override the choices made by `configure' or to help\nit to find libraries and programs with nonstandard names/locations.\n\nReport bugs to <https://github.com/abraunegg/onedrive>.\n_ACEOF\nac_status=$?\nfi\n\nif test \"$ac_init_help\" = \"recursive\"; then\n  # If there are subdirs, report their specific --help.\n  for ac_dir in : $ac_subdirs_all; do test \"x$ac_dir\" = x: && continue\n    test -d \"$ac_dir\" ||\n      { cd \"$srcdir\" && ac_pwd=`pwd` && srcdir=. && test -d \"$ac_dir\"; } ||\n      continue\n    ac_builddir=.\n\ncase \"$ac_dir\" in\n.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;;\n*)\n  ac_dir_suffix=/`$as_echo \"$ac_dir\" | sed 's|^\\.[\\\\/]||'`\n  # A \"..\" for each directory in $ac_dir_suffix.\n  ac_top_builddir_sub=`$as_echo \"$ac_dir_suffix\" | sed 's|/[^\\\\/]*|/..|g;s|/||'`\n  case $ac_top_builddir_sub in\n  \"\") ac_top_builddir_sub=. ac_top_build_prefix= ;;\n  *)  ac_top_build_prefix=$ac_top_builddir_sub/ ;;\n  esac ;;\nesac\nac_abs_top_builddir=$ac_pwd\nac_abs_builddir=$ac_pwd$ac_dir_suffix\n# for backward compatibility:\nac_top_builddir=$ac_top_build_prefix\n\ncase $srcdir in\n  .)  # We are building in place.\n    ac_srcdir=.\n    ac_top_srcdir=$ac_top_builddir_sub\n    ac_abs_top_srcdir=$ac_pwd ;;\n  [\\\\/]* | ?:[\\\\/]* )  # Absolute name.\n    ac_srcdir=$srcdir$ac_dir_suffix;\n    ac_top_srcdir=$srcdir\n    ac_abs_top_srcdir=$srcdir ;;\n  *) # Relative name.\n    ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix\n    ac_top_srcdir=$ac_top_build_prefix$srcdir\n    ac_abs_top_srcdir=$ac_pwd/$srcdir ;;\nesac\nac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix\n\n    cd \"$ac_dir\" || { ac_status=$?; continue; }\n    # Check for guested configure.\n    if test -f \"$ac_srcdir/configure.gnu\"; then\n      echo &&\n      $SHELL \"$ac_srcdir/configure.gnu\" --help=recursive\n    elif test -f \"$ac_srcdir/configure\"; then\n      echo &&\n      $SHELL \"$ac_srcdir/configure\" --help=recursive\n    else\n      $as_echo \"$as_me: WARNING: no configuration information is in $ac_dir\" >&2\n    fi || ac_status=$?\n    cd \"$ac_pwd\" || { ac_status=$?; break; }\n  done\nfi\n\ntest -n \"$ac_init_help\" && exit $ac_status\nif $ac_init_version; then\n  cat <<\\_ACEOF\nonedrive configure v2.5.10\ngenerated by GNU Autoconf 2.69\n\nCopyright (C) 2012 Free Software Foundation, Inc.\nThis configure script is free software; the Free Software Foundation\ngives unlimited permission to copy, distribute and modify it.\n_ACEOF\n  exit\nfi\n\n## ------------------------ ##\n## Autoconf initialization. ##\n## ------------------------ ##\ncat >config.log <<_ACEOF\nThis file contains any messages produced by compilers while\nrunning configure, to aid debugging if configure makes a mistake.\n\nIt was created by onedrive $as_me v2.5.10, which was\ngenerated by GNU Autoconf 2.69.  Invocation command line was\n\n  $ $0 $@\n\n_ACEOF\nexec 5>>config.log\n{\ncat <<_ASUNAME\n## --------- ##\n## Platform. ##\n## --------- ##\n\nhostname = `(hostname || uname -n) 2>/dev/null | sed 1q`\nuname -m = `(uname -m) 2>/dev/null || echo unknown`\nuname -r = `(uname -r) 2>/dev/null || echo unknown`\nuname -s = `(uname -s) 2>/dev/null || echo unknown`\nuname -v = `(uname -v) 2>/dev/null || echo unknown`\n\n/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null || echo unknown`\n/bin/uname -X     = `(/bin/uname -X) 2>/dev/null     || echo unknown`\n\n/bin/arch              = `(/bin/arch) 2>/dev/null              || echo unknown`\n/usr/bin/arch -k       = `(/usr/bin/arch -k) 2>/dev/null       || echo unknown`\n/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null || echo unknown`\n/usr/bin/hostinfo      = `(/usr/bin/hostinfo) 2>/dev/null      || echo unknown`\n/bin/machine           = `(/bin/machine) 2>/dev/null           || echo unknown`\n/usr/bin/oslevel       = `(/usr/bin/oslevel) 2>/dev/null       || echo unknown`\n/bin/universe          = `(/bin/universe) 2>/dev/null          || echo unknown`\n\n_ASUNAME\n\nas_save_IFS=$IFS; IFS=$PATH_SEPARATOR\nfor as_dir in $PATH\ndo\n  IFS=$as_save_IFS\n  test -z \"$as_dir\" && as_dir=.\n    $as_echo \"PATH: $as_dir\"\n  done\nIFS=$as_save_IFS\n\n} >&5\n\ncat >&5 <<_ACEOF\n\n\n## ----------- ##\n## Core tests. ##\n## ----------- ##\n\n_ACEOF\n\n\n# Keep a trace of the command line.\n# Strip out --no-create and --no-recursion so they do not pile up.\n# Strip out --silent because we don't want to record it for future runs.\n# Also quote any args containing shell meta-characters.\n# Make two passes to allow for proper duplicate-argument suppression.\nac_configure_args=\nac_configure_args0=\nac_configure_args1=\nac_must_keep_next=false\nfor ac_pass in 1 2\ndo\n  for ac_arg\n  do\n    case $ac_arg in\n    -no-create | --no-c* | -n | -no-recursion | --no-r*) continue ;;\n    -q | -quiet | --quiet | --quie | --qui | --qu | --q \\\n    | -silent | --silent | --silen | --sile | --sil)\n      continue ;;\n    *\\'*)\n      ac_arg=`$as_echo \"$ac_arg\" | sed \"s/'/'\\\\\\\\\\\\\\\\''/g\"` ;;\n    esac\n    case $ac_pass in\n    1) as_fn_append ac_configure_args0 \" '$ac_arg'\" ;;\n    2)\n      as_fn_append ac_configure_args1 \" '$ac_arg'\"\n      if test $ac_must_keep_next = true; then\n\tac_must_keep_next=false # Got value, back to normal.\n      else\n\tcase $ac_arg in\n\t  *=* | --config-cache | -C | -disable-* | --disable-* \\\n\t  | -enable-* | --enable-* | -gas | --g* | -nfp | --nf* \\\n\t  | -q | -quiet | --q* | -silent | --sil* | -v | -verb* \\\n\t  | -with-* | --with-* | -without-* | --without-* | --x)\n\t    case \"$ac_configure_args0 \" in\n\t      \"$ac_configure_args1\"*\" '$ac_arg' \"* ) continue ;;\n\t    esac\n\t    ;;\n\t  -* ) ac_must_keep_next=true ;;\n\tesac\n      fi\n      as_fn_append ac_configure_args \" '$ac_arg'\"\n      ;;\n    esac\n  done\ndone\n{ ac_configure_args0=; unset ac_configure_args0;}\n{ ac_configure_args1=; unset ac_configure_args1;}\n\n# When interrupted or exit'd, cleanup temporary files, and complete\n# config.log.  We remove comments because anyway the quotes in there\n# would cause problems or look ugly.\n# WARNING: Use '\\'' to represent an apostrophe within the trap.\n# WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug.\ntrap 'exit_status=$?\n  # Save into config.log some information that might help in debugging.\n  {\n    echo\n\n    $as_echo \"## ---------------- ##\n## Cache variables. ##\n## ---------------- ##\"\n    echo\n    # The following way of writing the cache mishandles newlines in values,\n(\n  for ac_var in `(set) 2>&1 | sed -n '\\''s/^\\([a-zA-Z_][a-zA-Z0-9_]*\\)=.*/\\1/p'\\''`; do\n    eval ac_val=\\$$ac_var\n    case $ac_val in #(\n    *${as_nl}*)\n      case $ac_var in #(\n      *_cv_*) { $as_echo \"$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline\" >&5\n$as_echo \"$as_me: WARNING: cache variable $ac_var contains a newline\" >&2;} ;;\n      esac\n      case $ac_var in #(\n      _ | IFS | as_nl) ;; #(\n      BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #(\n      *) { eval $ac_var=; unset $ac_var;} ;;\n      esac ;;\n    esac\n  done\n  (set) 2>&1 |\n    case $as_nl`(ac_space='\\'' '\\''; set) 2>&1` in #(\n    *${as_nl}ac_space=\\ *)\n      sed -n \\\n\t\"s/'\\''/'\\''\\\\\\\\'\\'''\\''/g;\n\t  s/^\\\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\\\)=\\\\(.*\\\\)/\\\\1='\\''\\\\2'\\''/p\"\n      ;; #(\n    *)\n      sed -n \"/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p\"\n      ;;\n    esac |\n    sort\n)\n    echo\n\n    $as_echo \"## ----------------- ##\n## Output variables. ##\n## ----------------- ##\"\n    echo\n    for ac_var in $ac_subst_vars\n    do\n      eval ac_val=\\$$ac_var\n      case $ac_val in\n      *\\'\\''*) ac_val=`$as_echo \"$ac_val\" | sed \"s/'\\''/'\\''\\\\\\\\\\\\\\\\'\\'''\\''/g\"`;;\n      esac\n      $as_echo \"$ac_var='\\''$ac_val'\\''\"\n    done | sort\n    echo\n\n    if test -n \"$ac_subst_files\"; then\n      $as_echo \"## ------------------- ##\n## File substitutions. ##\n## ------------------- ##\"\n      echo\n      for ac_var in $ac_subst_files\n      do\n\teval ac_val=\\$$ac_var\n\tcase $ac_val in\n\t*\\'\\''*) ac_val=`$as_echo \"$ac_val\" | sed \"s/'\\''/'\\''\\\\\\\\\\\\\\\\'\\'''\\''/g\"`;;\n\tesac\n\t$as_echo \"$ac_var='\\''$ac_val'\\''\"\n      done | sort\n      echo\n    fi\n\n    if test -s confdefs.h; then\n      $as_echo \"## ----------- ##\n## confdefs.h. ##\n## ----------- ##\"\n      echo\n      cat confdefs.h\n      echo\n    fi\n    test \"$ac_signal\" != 0 &&\n      $as_echo \"$as_me: caught signal $ac_signal\"\n    $as_echo \"$as_me: exit $exit_status\"\n  } >&5\n  rm -f core *.core core.conftest.* &&\n    rm -f -r conftest* confdefs* conf$$* $ac_clean_files &&\n    exit $exit_status\n' 0\nfor ac_signal in 1 2 13 15; do\n  trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal\ndone\nac_signal=0\n\n# confdefs.h avoids OS command line length limits that DEFS can exceed.\nrm -f -r conftest* confdefs.h\n\n$as_echo \"/* confdefs.h */\" > confdefs.h\n\n# Predefined preprocessor variables.\n\ncat >>confdefs.h <<_ACEOF\n#define PACKAGE_NAME \"$PACKAGE_NAME\"\n_ACEOF\n\ncat >>confdefs.h <<_ACEOF\n#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"\n_ACEOF\n\ncat >>confdefs.h <<_ACEOF\n#define PACKAGE_VERSION \"$PACKAGE_VERSION\"\n_ACEOF\n\ncat >>confdefs.h <<_ACEOF\n#define PACKAGE_STRING \"$PACKAGE_STRING\"\n_ACEOF\n\ncat >>confdefs.h <<_ACEOF\n#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"\n_ACEOF\n\ncat >>confdefs.h <<_ACEOF\n#define PACKAGE_URL \"$PACKAGE_URL\"\n_ACEOF\n\n\n# Let the site file select an alternate cache file if it wants to.\n# Prefer an explicitly selected file to automatically selected ones.\nac_site_file1=NONE\nac_site_file2=NONE\nif test -n \"$CONFIG_SITE\"; then\n  # We do not want a PATH search for config.site.\n  case $CONFIG_SITE in #((\n    -*)  ac_site_file1=./$CONFIG_SITE;;\n    */*) ac_site_file1=$CONFIG_SITE;;\n    *)   ac_site_file1=./$CONFIG_SITE;;\n  esac\nelif test \"x$prefix\" != xNONE; then\n  ac_site_file1=$prefix/share/config.site\n  ac_site_file2=$prefix/etc/config.site\nelse\n  ac_site_file1=$ac_default_prefix/share/config.site\n  ac_site_file2=$ac_default_prefix/etc/config.site\nfi\nfor ac_site_file in \"$ac_site_file1\" \"$ac_site_file2\"\ndo\n  test \"x$ac_site_file\" = xNONE && continue\n  if test /dev/null != \"$ac_site_file\" && test -r \"$ac_site_file\"; then\n    { $as_echo \"$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file\" >&5\n$as_echo \"$as_me: loading site script $ac_site_file\" >&6;}\n    sed 's/^/| /' \"$ac_site_file\" >&5\n    . \"$ac_site_file\" \\\n      || { { $as_echo \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n$as_echo \"$as_me: error: in \\`$ac_pwd':\" >&2;}\nas_fn_error $? \"failed to load site script $ac_site_file\nSee \\`config.log' for more details\" \"$LINENO\" 5; }\n  fi\ndone\n\nif test -r \"$cache_file\"; then\n  # Some versions of bash will fail to source /dev/null (special files\n  # actually), so we avoid doing that.  DJGPP emulates it as a regular file.\n  if test /dev/null != \"$cache_file\" && test -f \"$cache_file\"; then\n    { $as_echo \"$as_me:${as_lineno-$LINENO}: loading cache $cache_file\" >&5\n$as_echo \"$as_me: loading cache $cache_file\" >&6;}\n    case $cache_file in\n      [\\\\/]* | ?:[\\\\/]* ) . \"$cache_file\";;\n      *)                      . \"./$cache_file\";;\n    esac\n  fi\nelse\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: creating cache $cache_file\" >&5\n$as_echo \"$as_me: creating cache $cache_file\" >&6;}\n  >$cache_file\nfi\n\n# Check that the precious variables saved in the cache have kept the same\n# value.\nac_cache_corrupted=false\nfor ac_var in $ac_precious_vars; do\n  eval ac_old_set=\\$ac_cv_env_${ac_var}_set\n  eval ac_new_set=\\$ac_env_${ac_var}_set\n  eval ac_old_val=\\$ac_cv_env_${ac_var}_value\n  eval ac_new_val=\\$ac_env_${ac_var}_value\n  case $ac_old_set,$ac_new_set in\n    set,)\n      { $as_echo \"$as_me:${as_lineno-$LINENO}: error: \\`$ac_var' was set to \\`$ac_old_val' in the previous run\" >&5\n$as_echo \"$as_me: error: \\`$ac_var' was set to \\`$ac_old_val' in the previous run\" >&2;}\n      ac_cache_corrupted=: ;;\n    ,set)\n      { $as_echo \"$as_me:${as_lineno-$LINENO}: error: \\`$ac_var' was not set in the previous run\" >&5\n$as_echo \"$as_me: error: \\`$ac_var' was not set in the previous run\" >&2;}\n      ac_cache_corrupted=: ;;\n    ,);;\n    *)\n      if test \"x$ac_old_val\" != \"x$ac_new_val\"; then\n\t# differences in whitespace do not lead to failure.\n\tac_old_val_w=`echo x $ac_old_val`\n\tac_new_val_w=`echo x $ac_new_val`\n\tif test \"$ac_old_val_w\" != \"$ac_new_val_w\"; then\n\t  { $as_echo \"$as_me:${as_lineno-$LINENO}: error: \\`$ac_var' has changed since the previous run:\" >&5\n$as_echo \"$as_me: error: \\`$ac_var' has changed since the previous run:\" >&2;}\n\t  ac_cache_corrupted=:\n\telse\n\t  { $as_echo \"$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \\`$ac_var' since the previous run:\" >&5\n$as_echo \"$as_me: warning: ignoring whitespace changes in \\`$ac_var' since the previous run:\" >&2;}\n\t  eval $ac_var=\\$ac_old_val\n\tfi\n\t{ $as_echo \"$as_me:${as_lineno-$LINENO}:   former value:  \\`$ac_old_val'\" >&5\n$as_echo \"$as_me:   former value:  \\`$ac_old_val'\" >&2;}\n\t{ $as_echo \"$as_me:${as_lineno-$LINENO}:   current value: \\`$ac_new_val'\" >&5\n$as_echo \"$as_me:   current value: \\`$ac_new_val'\" >&2;}\n      fi;;\n  esac\n  # Pass precious variables to config.status.\n  if test \"$ac_new_set\" = set; then\n    case $ac_new_val in\n    *\\'*) ac_arg=$ac_var=`$as_echo \"$ac_new_val\" | sed \"s/'/'\\\\\\\\\\\\\\\\''/g\"` ;;\n    *) ac_arg=$ac_var=$ac_new_val ;;\n    esac\n    case \" $ac_configure_args \" in\n      *\" '$ac_arg' \"*) ;; # Avoid dups.  Use of quotes ensures accuracy.\n      *) as_fn_append ac_configure_args \" '$ac_arg'\" ;;\n    esac\n  fi\ndone\nif $ac_cache_corrupted; then\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n$as_echo \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build\" >&5\n$as_echo \"$as_me: error: changes in the environment can compromise the build\" >&2;}\n  as_fn_error $? \"run \\`make distclean' and/or \\`rm $cache_file' and start over\" \"$LINENO\" 5\nfi\n## -------------------- ##\n## Main body of script. ##\n## -------------------- ##\n\nac_ext=c\nac_cpp='$CPP $CPPFLAGS'\nac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'\nac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'\nac_compiler_gnu=$ac_cv_c_compiler_gnu\n\n\n\n\n\n\n\n\nac_aux_dir=\nfor ac_dir in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"; do\n  if test -f \"$ac_dir/install-sh\"; then\n    ac_aux_dir=$ac_dir\n    ac_install_sh=\"$ac_aux_dir/install-sh -c\"\n    break\n  elif test -f \"$ac_dir/install.sh\"; then\n    ac_aux_dir=$ac_dir\n    ac_install_sh=\"$ac_aux_dir/install.sh -c\"\n    break\n  elif test -f \"$ac_dir/shtool\"; then\n    ac_aux_dir=$ac_dir\n    ac_install_sh=\"$ac_aux_dir/shtool install -c\"\n    break\n  fi\ndone\nif test -z \"$ac_aux_dir\"; then\n  as_fn_error $? \"cannot find install-sh, install.sh, or shtool in \\\"$srcdir\\\" \\\"$srcdir/..\\\" \\\"$srcdir/../..\\\"\" \"$LINENO\" 5\nfi\n\n# These three variables are undocumented and unsupported,\n# and are intended to be withdrawn in a future Autoconf release.\n# They can cause serious problems if a builder's source tree is in a directory\n# whose full name contains unusual characters.\nac_config_guess=\"$SHELL $ac_aux_dir/config.guess\"  # Please don't use this var.\nac_config_sub=\"$SHELL $ac_aux_dir/config.sub\"  # Please don't use this var.\nac_configure=\"$SHELL $ac_aux_dir/configure\"  # Please don't use this var.\n\n\n# Find a good install program.  We prefer a C program (faster),\n# so one script is as good as another.  But avoid the broken or\n# incompatible versions:\n# SysV /etc/install, /usr/sbin/install\n# SunOS /usr/etc/install\n# IRIX /sbin/install\n# AIX /bin/install\n# AmigaOS /C/install, which installs bootblocks on floppy discs\n# AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag\n# AFS /usr/afsws/bin/install, which mishandles nonexistent args\n# SVR4 /usr/ucb/install, which tries to use the nonexistent group \"staff\"\n# OS/2's system install, which has a completely different semantic\n# ./install, which can be erroneously created by make from ./install.sh.\n# Reject install programs that cannot install multiple files.\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install\" >&5\n$as_echo_n \"checking for a BSD-compatible install... \" >&6; }\nif test -z \"$INSTALL\"; then\nif ${ac_cv_path_install+:} false; then :\n  $as_echo_n \"(cached) \" >&6\nelse\n  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\nfor as_dir in $PATH\ndo\n  IFS=$as_save_IFS\n  test -z \"$as_dir\" && as_dir=.\n    # Account for people who put trailing slashes in PATH elements.\ncase $as_dir/ in #((\n  ./ | .// | /[cC]/* | \\\n  /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \\\n  ?:[\\\\/]os2[\\\\/]install[\\\\/]* | ?:[\\\\/]OS2[\\\\/]INSTALL[\\\\/]* | \\\n  /usr/ucb/* ) ;;\n  *)\n    # OSF1 and SCO ODT 3.0 have their own names for install.\n    # Don't use installbsd from OSF since it installs stuff as root\n    # by default.\n    for ac_prog in ginstall scoinst install; do\n      for ac_exec_ext in '' $ac_executable_extensions; do\n\tif as_fn_executable_p \"$as_dir/$ac_prog$ac_exec_ext\"; then\n\t  if test $ac_prog = install &&\n\t    grep dspmsg \"$as_dir/$ac_prog$ac_exec_ext\" >/dev/null 2>&1; then\n\t    # AIX install.  It has an incompatible calling convention.\n\t    :\n\t  elif test $ac_prog = install &&\n\t    grep pwplus \"$as_dir/$ac_prog$ac_exec_ext\" >/dev/null 2>&1; then\n\t    # program-specific install script used by HP pwplus--don't use.\n\t    :\n\t  else\n\t    rm -rf conftest.one conftest.two conftest.dir\n\t    echo one > conftest.one\n\t    echo two > conftest.two\n\t    mkdir conftest.dir\n\t    if \"$as_dir/$ac_prog$ac_exec_ext\" -c conftest.one conftest.two \"`pwd`/conftest.dir\" &&\n\t      test -s conftest.one && test -s conftest.two &&\n\t      test -s conftest.dir/conftest.one &&\n\t      test -s conftest.dir/conftest.two\n\t    then\n\t      ac_cv_path_install=\"$as_dir/$ac_prog$ac_exec_ext -c\"\n\t      break 3\n\t    fi\n\t  fi\n\tfi\n      done\n    done\n    ;;\nesac\n\n  done\nIFS=$as_save_IFS\n\nrm -rf conftest.one conftest.two conftest.dir\n\nfi\n  if test \"${ac_cv_path_install+set}\" = set; then\n    INSTALL=$ac_cv_path_install\n  else\n    # As a last resort, use the slow shell script.  Don't cache a\n    # value for INSTALL within a source directory, because that will\n    # break other packages using the cache if that directory is\n    # removed, or if the value is a relative name.\n    INSTALL=$ac_install_sh\n  fi\nfi\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: $INSTALL\" >&5\n$as_echo \"$INSTALL\" >&6; }\n\n# Use test -z because SunOS4 sh mishandles braces in ${var-val}.\n# It thinks the first close brace ends the variable substitution.\ntest -z \"$INSTALL_PROGRAM\" && INSTALL_PROGRAM='${INSTALL}'\n\ntest -z \"$INSTALL_SCRIPT\" && INSTALL_SCRIPT='${INSTALL}'\n\ntest -z \"$INSTALL_DATA\" && INSTALL_DATA='${INSTALL} -m 644'\n\n\n\n\n\n\n\n\nif test \"x$ac_cv_env_PKG_CONFIG_set\" != \"xset\"; then\n\tif test -n \"$ac_tool_prefix\"; then\n  # Extract the first word of \"${ac_tool_prefix}pkg-config\", so it can be a program name with args.\nset dummy ${ac_tool_prefix}pkg-config; ac_word=$2\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking for $ac_word\" >&5\n$as_echo_n \"checking for $ac_word... \" >&6; }\nif ${ac_cv_path_PKG_CONFIG+:} false; then :\n  $as_echo_n \"(cached) \" >&6\nelse\n  case $PKG_CONFIG in\n  [\\\\/]* | ?:[\\\\/]*)\n  ac_cv_path_PKG_CONFIG=\"$PKG_CONFIG\" # Let the user override the test with a path.\n  ;;\n  *)\n  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\nfor as_dir in $PATH\ndo\n  IFS=$as_save_IFS\n  test -z \"$as_dir\" && as_dir=.\n    for ac_exec_ext in '' $ac_executable_extensions; do\n  if as_fn_executable_p \"$as_dir/$ac_word$ac_exec_ext\"; then\n    ac_cv_path_PKG_CONFIG=\"$as_dir/$ac_word$ac_exec_ext\"\n    $as_echo \"$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext\" >&5\n    break 2\n  fi\ndone\n  done\nIFS=$as_save_IFS\n\n  ;;\nesac\nfi\nPKG_CONFIG=$ac_cv_path_PKG_CONFIG\nif test -n \"$PKG_CONFIG\"; then\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: result: $PKG_CONFIG\" >&5\n$as_echo \"$PKG_CONFIG\" >&6; }\nelse\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\nfi\n\n\nfi\nif test -z \"$ac_cv_path_PKG_CONFIG\"; then\n  ac_pt_PKG_CONFIG=$PKG_CONFIG\n  # Extract the first word of \"pkg-config\", so it can be a program name with args.\nset dummy pkg-config; ac_word=$2\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking for $ac_word\" >&5\n$as_echo_n \"checking for $ac_word... \" >&6; }\nif ${ac_cv_path_ac_pt_PKG_CONFIG+:} false; then :\n  $as_echo_n \"(cached) \" >&6\nelse\n  case $ac_pt_PKG_CONFIG in\n  [\\\\/]* | ?:[\\\\/]*)\n  ac_cv_path_ac_pt_PKG_CONFIG=\"$ac_pt_PKG_CONFIG\" # Let the user override the test with a path.\n  ;;\n  *)\n  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\nfor as_dir in $PATH\ndo\n  IFS=$as_save_IFS\n  test -z \"$as_dir\" && as_dir=.\n    for ac_exec_ext in '' $ac_executable_extensions; do\n  if as_fn_executable_p \"$as_dir/$ac_word$ac_exec_ext\"; then\n    ac_cv_path_ac_pt_PKG_CONFIG=\"$as_dir/$ac_word$ac_exec_ext\"\n    $as_echo \"$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext\" >&5\n    break 2\n  fi\ndone\n  done\nIFS=$as_save_IFS\n\n  ;;\nesac\nfi\nac_pt_PKG_CONFIG=$ac_cv_path_ac_pt_PKG_CONFIG\nif test -n \"$ac_pt_PKG_CONFIG\"; then\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: result: $ac_pt_PKG_CONFIG\" >&5\n$as_echo \"$ac_pt_PKG_CONFIG\" >&6; }\nelse\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\nfi\n\n  if test \"x$ac_pt_PKG_CONFIG\" = x; then\n    PKG_CONFIG=\"\"\n  else\n    case $cross_compiling:$ac_tool_warned in\nyes:)\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet\" >&5\n$as_echo \"$as_me: WARNING: using cross tools not prefixed with host triplet\" >&2;}\nac_tool_warned=yes ;;\nesac\n    PKG_CONFIG=$ac_pt_PKG_CONFIG\n  fi\nelse\n  PKG_CONFIG=\"$ac_cv_path_PKG_CONFIG\"\nfi\n\nfi\nif test -n \"$PKG_CONFIG\"; then\n\t_pkg_min_version=0.9.0\n\t{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking pkg-config is at least version $_pkg_min_version\" >&5\n$as_echo_n \"checking pkg-config is at least version $_pkg_min_version... \" >&6; }\n\tif $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then\n\t\t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n$as_echo \"yes\" >&6; }\n\telse\n\t\t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\t\tPKG_CONFIG=\"\"\n\tfi\nfi\n\nPACKAGE_DATE=\"January 2026\"\n\n\nfor ac_prog in dmd ldmd2 ldc2 gdmd gdc\ndo\n  # Extract the first word of \"$ac_prog\", so it can be a program name with args.\nset dummy $ac_prog; ac_word=$2\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking for $ac_word\" >&5\n$as_echo_n \"checking for $ac_word... \" >&6; }\nif ${ac_cv_prog_DC+:} false; then :\n  $as_echo_n \"(cached) \" >&6\nelse\n  if test -n \"$DC\"; then\n  ac_cv_prog_DC=\"$DC\" # Let the user override the test.\nelse\nas_save_IFS=$IFS; IFS=$PATH_SEPARATOR\nfor as_dir in $PATH\ndo\n  IFS=$as_save_IFS\n  test -z \"$as_dir\" && as_dir=.\n    for ac_exec_ext in '' $ac_executable_extensions; do\n  if as_fn_executable_p \"$as_dir/$ac_word$ac_exec_ext\"; then\n    ac_cv_prog_DC=\"$ac_prog\"\n    $as_echo \"$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext\" >&5\n    break 2\n  fi\ndone\n  done\nIFS=$as_save_IFS\n\nfi\nfi\nDC=$ac_cv_prog_DC\nif test -n \"$DC\"; then\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: result: $DC\" >&5\n$as_echo \"$DC\" >&6; }\nelse\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\nfi\n\n\n  test -n \"$DC\" && break\ndone\ntest -n \"$DC\" || DC=\"NOT_FOUND\"\n\nDC_TYPE=\ncase $(basename $DC) in\n        *ldc2*) DC_TYPE=ldc ;;\n        *gdc*) DC_TYPE=gdc ;;\n        *dmd*) DC_TYPE=dmd ;;\n        NOT_FOUND) as_fn_error 1 \"Could not find any compatible D compiler\" \"$LINENO\" 5\nesac\n\nvercomp () {\n    IFS=. read -r a0 a1 a2 aa <<EOF\n$1\nEOF\n    IFS=. read -r b0 b1 b2 bb <<EOF\n$2\nEOF\n    # leading 0 are ignored: 01 == 1, this also\n    # converts empty strings into 0: 1..2 == 1.0.2\n    a0=$(expr $a0 + 0)\n    a1=$(expr $a1 + 0)\n    a2=$(expr $a2 + 0)\n    b0=$(expr $b0 + 0)\n    b1=$(expr $b1 + 0)\n    b2=$(expr $b2 + 0)\n    #echo \"$1 parsed to a=$a0 b=$a1 c=$a2 rest=$aa\"\n    #echo \"$2 parsed to a=$b0 b=$b1 c=$b2 rest=$bb\"\n    if test $a0 -lt $b0\n    then\n      return 2\n    elif test $a0 -gt $b0\n    then\n      return 1\n    else\n      if test $a1 -lt $b1\n      then\n        return 2\n      elif test $a1 -gt $b1\n      then\n        return 1\n      else\n        if test $a2 -lt $b2\n\tthen\n\t  return 2\n\telif test $a2 -gt $b2\n\tthen\n\t  return 1\n\telse\n\t  if test $aa '<' $bb\n\t  then\n\t    return 2\n\t  elif test $aa '>' $bb\n\t  then\n\t    return 1\n          else\n\t    return 0\n\t  fi\n\tfi\n      fi\n    fi\n}\n\nDO_VERSION_CHECK=1\n# Check whether --enable-version-check was given.\nif test \"${enable_version_check+set}\" = set; then :\n  enableval=$enable_version_check;\nfi\n\nif test \"x$enable_version_check\" = \"xno\"; then :\n  DO_VERSION_CHECK=0\nfi\n\nif test \"$DO_VERSION_CHECK\" = \"1\"; then :\n   { $as_echo \"$as_me:${as_lineno-$LINENO}: checking version of D compiler\" >&5\n$as_echo_n \"checking version of D compiler... \" >&6; }\n# check for valid versions\ncase $(basename $DC) in\n\t*ldmd2*|*ldc2*)\n\t\t# LDC - the LLVM D compiler (1.12.0): ...\n\t\tVERSION=`$DC --version`\n\t\t# remove  everything up to first (\n\t\tVERSION=${VERSION#* (}\n\t\t# remove everything after ):\n\t\tVERSION=${VERSION%%):*}\n\t\t# now version should be something like L.M.N\n\t\tMINVERSION=1.20.1\n\t\t;;\n\t*gdmd*|*gdc*)\n\t\t# Both gdmd and gdc print the same version information\n\t\tVERSION=`${DC} --version | head -n1`\n\t\t# Some examples of output:\n\t\t# gdc (Gentoo 14.2.1_p20250301 p8) 14.2.1 20250301\n\t\t# gcc (GCC) 14.2.1 20250207 # Arch\n\t\t# gdc (GCC) 14.2.1 20250110 (Red Hat 14.2.1-7)\n\t\t# gdc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0\n\t\tVERSION=${VERSION#gdc }\n\t\t# VERSION=(...) VER DATE ...\n\t\tVERSION=${VERSION#*) }\n\t\t# VERSION=VER DATE ...\n\t\tVERSION=${VERSION%% *}\n\t\tMINVERSION=15\n\t\t;;\n\t*dmd*)\n\t\t# DMD64 D Compiler v2.085.1\\n...\n\t\tVERSION=`$DC --version | tr '\\n' ' '`\n\t\tVERSION=${VERSION#*Compiler v}\n\t\tVERSION=${VERSION%% *}\n\t\t# now version should be something like L.M.N\n\t\tMINVERSION=2.091.1\n\t\t;;\nesac\n\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: $VERSION\" >&5\n$as_echo \"$VERSION\" >&6; }\n\nvercomp $MINVERSION $VERSION\nif test $? = 1\nthen\n\tas_fn_error 1 \"Compiler version insufficient, current compiler version $VERSION, minimum version $MINVERSION\" \"$LINENO\" 5\nfi\n#echo \"MINVERSION=$MINVERSION VERSION=$VERSION\"\n\nfi\n\n\n\n\n\ncase \"$DC_TYPE\" in\n\tdmd)\n\t\tDEBUG_DCFLAGS=\"-g -debug -gs\"\n\t\tRELEASE_DCFLAGS=-O\n\t\tVERSION_DCFLAG=-version\n\t\tLINKER_DCFLAG=-L\n\t\tOUTPUT_DCFLAG=-of\n\t\tWERROR_DCFLAG=-w\n\t\t;;\n\tldc)\n\t\tDEBUG_DCFLAGS=\"-g -d-debug -gc\"\n\t\tRELEASE_DCFLAGS=-O\n\t\tVERSION_DCFLAG=-d-version\n\t\tLINKER_DCFLAG=-L\n\t\tOUTPUT_DCFLAG=-of\n\t\tWERROR_DCFLAG=-w\n\t\t;;\n\tgdc)\n\t\tDEBUG_DCFLAGS=\"-g -fdebug\"\n\t\tRELEASE_DCFLAGS=-O\n\t\tVERSION_DCFLAG=-fversion\n\t\tLINKER_DCFLAG=-Wl,\n\t\tOUTPUT_DCFLAG=-o\n\t\tWERROR_DCFLAG=-Werror\n\t\t;;\nesac\n\n\n\n\n\n\n\n\npkg_failed=no\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking for curl\" >&5\n$as_echo_n \"checking for curl... \" >&6; }\n\nif test -n \"$curl_CFLAGS\"; then\n    pkg_cv_curl_CFLAGS=\"$curl_CFLAGS\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"libcurl\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"libcurl\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_curl_CFLAGS=`$PKG_CONFIG --cflags \"libcurl\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\nif test -n \"$curl_LIBS\"; then\n    pkg_cv_curl_LIBS=\"$curl_LIBS\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"libcurl\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"libcurl\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_curl_LIBS=`$PKG_CONFIG --libs \"libcurl\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\n\n\n\nif test $pkg_failed = yes; then\n   \t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\nif $PKG_CONFIG --atleast-pkgconfig-version 0.20; then\n        _pkg_short_errors_supported=yes\nelse\n        _pkg_short_errors_supported=no\nfi\n        if test $_pkg_short_errors_supported = yes; then\n\t        curl_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs \"libcurl\" 2>&1`\n        else\n\t        curl_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs \"libcurl\" 2>&1`\n        fi\n\t# Put the nasty error message in config.log where it belongs\n\techo \"$curl_PKG_ERRORS\" >&5\n\n\tas_fn_error $? \"Package requirements (libcurl) were not met:\n\n$curl_PKG_ERRORS\n\nConsider adjusting the PKG_CONFIG_PATH environment variable if you\ninstalled software in a non-standard prefix.\n\nAlternatively, you may set the environment variables curl_CFLAGS\nand curl_LIBS to avoid the need to call pkg-config.\nSee the pkg-config man page for more details.\" \"$LINENO\" 5\nelif test $pkg_failed = untried; then\n     \t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\t{ { $as_echo \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n$as_echo \"$as_me: error: in \\`$ac_pwd':\" >&2;}\nas_fn_error $? \"The pkg-config script could not be found or is too old.  Make sure it\nis in your PATH or set the PKG_CONFIG environment variable to the full\npath to pkg-config.\n\nAlternatively, you may set the environment variables curl_CFLAGS\nand curl_LIBS to avoid the need to call pkg-config.\nSee the pkg-config man page for more details.\n\nTo get pkg-config, see <http://pkg-config.freedesktop.org/>.\nSee \\`config.log' for more details\" \"$LINENO\" 5; }\nelse\n\tcurl_CFLAGS=$pkg_cv_curl_CFLAGS\n\tcurl_LIBS=$pkg_cv_curl_LIBS\n        { $as_echo \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n$as_echo \"yes\" >&6; }\n\nfi\n\npkg_failed=no\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking for sqlite\" >&5\n$as_echo_n \"checking for sqlite... \" >&6; }\n\nif test -n \"$sqlite_CFLAGS\"; then\n    pkg_cv_sqlite_CFLAGS=\"$sqlite_CFLAGS\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"sqlite3\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"sqlite3\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_sqlite_CFLAGS=`$PKG_CONFIG --cflags \"sqlite3\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\nif test -n \"$sqlite_LIBS\"; then\n    pkg_cv_sqlite_LIBS=\"$sqlite_LIBS\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"sqlite3\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"sqlite3\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_sqlite_LIBS=`$PKG_CONFIG --libs \"sqlite3\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\n\n\n\nif test $pkg_failed = yes; then\n   \t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\nif $PKG_CONFIG --atleast-pkgconfig-version 0.20; then\n        _pkg_short_errors_supported=yes\nelse\n        _pkg_short_errors_supported=no\nfi\n        if test $_pkg_short_errors_supported = yes; then\n\t        sqlite_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs \"sqlite3\" 2>&1`\n        else\n\t        sqlite_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs \"sqlite3\" 2>&1`\n        fi\n\t# Put the nasty error message in config.log where it belongs\n\techo \"$sqlite_PKG_ERRORS\" >&5\n\n\tas_fn_error $? \"Package requirements (sqlite3) were not met:\n\n$sqlite_PKG_ERRORS\n\nConsider adjusting the PKG_CONFIG_PATH environment variable if you\ninstalled software in a non-standard prefix.\n\nAlternatively, you may set the environment variables sqlite_CFLAGS\nand sqlite_LIBS to avoid the need to call pkg-config.\nSee the pkg-config man page for more details.\" \"$LINENO\" 5\nelif test $pkg_failed = untried; then\n     \t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\t{ { $as_echo \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n$as_echo \"$as_me: error: in \\`$ac_pwd':\" >&2;}\nas_fn_error $? \"The pkg-config script could not be found or is too old.  Make sure it\nis in your PATH or set the PKG_CONFIG environment variable to the full\npath to pkg-config.\n\nAlternatively, you may set the environment variables sqlite_CFLAGS\nand sqlite_LIBS to avoid the need to call pkg-config.\nSee the pkg-config man page for more details.\n\nTo get pkg-config, see <http://pkg-config.freedesktop.org/>.\nSee \\`config.log' for more details\" \"$LINENO\" 5; }\nelse\n\tsqlite_CFLAGS=$pkg_cv_sqlite_CFLAGS\n\tsqlite_LIBS=$pkg_cv_sqlite_LIBS\n        { $as_echo \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n$as_echo \"yes\" >&6; }\n\nfi\n\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking whether to enable dbus support\" >&5\n$as_echo_n \"checking whether to enable dbus support... \" >&6; }\ncase \"$(uname -s)\" in\n  Linux)\n    enable_dbus=yes\n    { $as_echo \"$as_me:${as_lineno-$LINENO}: result: yes (on Linux)\" >&5\n$as_echo \"yes (on Linux)\" >&6; }\n\npkg_failed=no\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking for dbus\" >&5\n$as_echo_n \"checking for dbus... \" >&6; }\n\nif test -n \"$dbus_CFLAGS\"; then\n    pkg_cv_dbus_CFLAGS=\"$dbus_CFLAGS\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"dbus-1 >= 1.0\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"dbus-1 >= 1.0\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_dbus_CFLAGS=`$PKG_CONFIG --cflags \"dbus-1 >= 1.0\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\nif test -n \"$dbus_LIBS\"; then\n    pkg_cv_dbus_LIBS=\"$dbus_LIBS\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"dbus-1 >= 1.0\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"dbus-1 >= 1.0\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_dbus_LIBS=`$PKG_CONFIG --libs \"dbus-1 >= 1.0\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\n\n\n\nif test $pkg_failed = yes; then\n   \t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\nif $PKG_CONFIG --atleast-pkgconfig-version 0.20; then\n        _pkg_short_errors_supported=yes\nelse\n        _pkg_short_errors_supported=no\nfi\n        if test $_pkg_short_errors_supported = yes; then\n\t        dbus_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs \"dbus-1 >= 1.0\" 2>&1`\n        else\n\t        dbus_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs \"dbus-1 >= 1.0\" 2>&1`\n        fi\n\t# Put the nasty error message in config.log where it belongs\n\techo \"$dbus_PKG_ERRORS\" >&5\n\n\tas_fn_error $? \"dbus-1 development files not found. Please install dbus-devel (Red Hat), libdbus-1-dev (Debian) or dbus (Arch | Manjaro)\" \"$LINENO\" 5\n\nelif test $pkg_failed = untried; then\n     \t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\tas_fn_error $? \"dbus-1 development files not found. Please install dbus-devel (Red Hat), libdbus-1-dev (Debian) or dbus (Arch | Manjaro)\" \"$LINENO\" 5\n\nelse\n\tdbus_CFLAGS=$pkg_cv_dbus_CFLAGS\n\tdbus_LIBS=$pkg_cv_dbus_LIBS\n        { $as_echo \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n$as_echo \"yes\" >&6; }\n\n$as_echo \"#define HAVE_DBUS 1\" >>confdefs.h\n\n\nfi\n    ;;\n  *)\n    enable_dbus=no\n    { $as_echo \"$as_me:${as_lineno-$LINENO}: result: no (not on Linux)\" >&5\n$as_echo \"no (not on Linux)\" >&6; }\n    ;;\nesac\n\n\n\n\n\n\n# Check whether --with-systemdsystemunitdir was given.\nif test \"${with_systemdsystemunitdir+set}\" = set; then :\n  withval=$with_systemdsystemunitdir;\nelse\n  with_systemdsystemunitdir=auto\nfi\n\nif test \"x$with_systemdsystemunitdir\" = \"xyes\" -o \"x$with_systemdsystemunitdir\" = \"xauto\"; then :\n           def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)\n                        if test \"x$def_systemdsystemunitdir\" = \"x\"; then :\n                    if test \"x$with_systemdsystemunitdir\" = \"xyes\"; then :\n  as_fn_error $? \"systemd support requested but pkg-config unable to query systemd package\" \"$LINENO\" 5\nfi\n                with_systemdsystemunitdir=no\n\nelse\n                   with_systemdsystemunitdir=\"$def_systemdsystemunitdir\"\n\n\nfi\n\n\nfi\nif test \"x$with_systemdsystemunitdir\" != \"xno\"; then :\n  systemdsystemunitdir=$with_systemdsystemunitdir\n\nfi\n\n\n# Check whether --with-systemduserunitdir was given.\nif test \"${with_systemduserunitdir+set}\" = set; then :\n  withval=$with_systemduserunitdir;\nelse\n  with_systemduserunitdir=auto\nfi\n\nif test \"x$with_systemduserunitdir\" = \"xyes\" -o \"x$with_systemduserunitdir\" = \"xauto\"; then :\n\n        def_systemduserunitdir=$($PKG_CONFIG --variable=systemduserunitdir systemd)\n        if test \"x$def_systemduserunitdir\" = \"x\"; then :\n\n                if test \"x$with_systemduserunitdir\" = \"xyes\"; then :\n  as_fn_error $? \"systemd support requested but pkg-config unable to query systemd package\" \"$LINENO\" 5\nfi\n                with_systemduserunitdir=no\n\nelse\n\n                with_systemduserunitdir=\"$def_systemduserunitdir\"\n\n\nfi\n\n\nfi\nif test \"x$with_systemduserunitdir\" != \"xno\"; then :\n  systemduserunitdir=$with_systemduserunitdir\n\nfi\n\nif test \"x$with_systemduserunitdir\" != \"xno\" -a \"x$with_systemdsystemunitdir\" != \"xno\"; then :\n  havesystemd=yes\nelse\n  havesystemd=no\nfi\nHAVE_SYSTEMD=$havesystemd\n\n\n\n\n# Check whether --enable-notifications was given.\nif test \"${enable_notifications+set}\" = set; then :\n  enableval=$enable_notifications;\nfi\n\n\nif test \"x$enable_notifications\" = \"xyes\"; then :\n  enable_notifications=yes\nelse\n  enable_notifications=no\nfi\n\nif test \"x$enable_notifications\" = \"xyes\"; then :\n\npkg_failed=no\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: checking for notify\" >&5\n$as_echo_n \"checking for notify... \" >&6; }\n\nif test -n \"$notify_CFLAGS\"; then\n    pkg_cv_notify_CFLAGS=\"$notify_CFLAGS\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"libnotify\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"libnotify\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_notify_CFLAGS=`$PKG_CONFIG --cflags \"libnotify\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\nif test -n \"$notify_LIBS\"; then\n    pkg_cv_notify_LIBS=\"$notify_LIBS\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"libnotify\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"libnotify\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_notify_LIBS=`$PKG_CONFIG --libs \"libnotify\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\n\n\n\nif test $pkg_failed = yes; then\n   \t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\nif $PKG_CONFIG --atleast-pkgconfig-version 0.20; then\n        _pkg_short_errors_supported=yes\nelse\n        _pkg_short_errors_supported=no\nfi\n        if test $_pkg_short_errors_supported = yes; then\n\t        notify_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs \"libnotify\" 2>&1`\n        else\n\t        notify_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs \"libnotify\" 2>&1`\n        fi\n\t# Put the nasty error message in config.log where it belongs\n\techo \"$notify_PKG_ERRORS\" >&5\n\n\tenable_notifications=no\nelif test $pkg_failed = untried; then\n     \t{ $as_echo \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n$as_echo \"no\" >&6; }\n\tenable_notifications=no\nelse\n\tnotify_CFLAGS=$pkg_cv_notify_CFLAGS\n\tnotify_LIBS=$pkg_cv_notify_LIBS\n        { $as_echo \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n$as_echo \"yes\" >&6; }\n\nfi\nelse\n  notify_LIBS=\"\"\n\nfi\nNOTIFICATIONS=$enable_notifications\n\n\n\n# Conditionally set bsd_inotify_LIBS based on the platform\ncase \"$(uname -s)\" in\n    Linux)\n        bsd_inotify_LIBS=\"\"\n        ;;\n    FreeBSD)\n        if test \"$(uname -U)\" -gt 1500060; then :\n  bsd_inotify_LIBS=\"\"\nelse\n  bsd_inotify_LIBS=\"-L/usr/local/lib -linotify\"\n\nfi\n        ;;\n    OpenBSD)\n        bsd_inotify_LIBS=\"-L/usr/local/lib/inotify -linotify\"\n        ;;\n    *)\n        bsd_inotify_LIBS=\"\"\n        ;;\nesac\n\n\n\n\n# Conditionally set dynamic_linker_LIBS based on the platform\ncase \"$(uname -s)\" in\n    Linux)\n        dynamic_linker_LIBS=\"-ldl\"\n        ;;\n    *)\n        dynamic_linker_LIBS=\"\"\n        ;;\nesac\n\n\n\n\n# Check whether --enable-completions was given.\nif test \"${enable_completions+set}\" = set; then :\n  enableval=$enable_completions;\nfi\n\n\nif test \"x$enable_completions\" = \"xyes\"; then :\n  enable_completions=yes\nelse\n  enable_completions=no\nfi\n\nCOMPLETIONS=$enable_completions\n\n\n\nif test \"x$enable_completions\" = \"xyes\"; then :\n\n\n# Check whether --with-bash-completion-dir was given.\nif test \"${with_bash_completion_dir+set}\" = set; then :\n  withval=$with_bash_completion_dir;\nelse\n  with_bash_completion_dir=auto\nfi\n\n  if test \"x$with_bash_completion_dir\" = \"xyes\" -o \"x$with_bash_completion_dir\" = \"xauto\"; then :\n\n\nif test -n \"$bashcompdir\"; then\n    pkg_cv_bashcompdir=\"$bashcompdir\"\n elif test -n \"$PKG_CONFIG\"; then\n    if test -n \"$PKG_CONFIG\" && \\\n    { { $as_echo \"$as_me:${as_lineno-$LINENO}: \\$PKG_CONFIG --exists --print-errors \\\"bash-completion\\\"\"; } >&5\n  ($PKG_CONFIG --exists --print-errors \"bash-completion\") 2>&5\n  ac_status=$?\n  $as_echo \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n  test $ac_status = 0; }; then\n  pkg_cv_bashcompdir=`$PKG_CONFIG --variable=\"completionsdir\" \"bash-completion\" 2>/dev/null`\n\t\t      test \"x$?\" != \"x0\" && pkg_failed=yes\nelse\n  pkg_failed=yes\nfi\n else\n    pkg_failed=untried\nfi\nbashcompdir=$pkg_cv_bashcompdir\n\nif test \"x$bashcompdir\" = x\"\"; then :\n  bashcompdir=\"${sysconfdir}/bash_completion.d\"\nfi\n      with_bash_completion_dir=$bashcompdir\n\nfi\n  BASH_COMPLETION_DIR=$with_bash_completion_dir\n\n\n\n# Check whether --with-zsh-completion-dir was given.\nif test \"${with_zsh_completion_dir+set}\" = set; then :\n  withval=$with_zsh_completion_dir;\nelse\n  with_zsh_completion_dir=auto\nfi\n\n  if test \"x$with_zsh_completion_dir\" = \"xyes\" -o \"x$with_zsh_completion_dir\" = \"xauto\"; then :\n\n      with_zsh_completion_dir=\"/usr/local/share/zsh/site-functions\"\n\nfi\n  ZSH_COMPLETION_DIR=$with_zsh_completion_dir\n\n\n\n# Check whether --with-fish-completion-dir was given.\nif test \"${with_fish_completion_dir+set}\" = set; then :\n  withval=$with_fish_completion_dir;\nelse\n  with_fish_completion_dir=auto\nfi\n\n  if test \"x$with_fish_completion_dir\" = \"xyes\" -o \"x$with_fish_completion_dir\" = \"xauto\"; then :\n\n     with_fish_completion_dir=\"/usr/local/share/fish/completions\"\n\nfi\n  FISH_COMPLETION_DIR=$with_fish_completion_dir\n\n\n\nfi\n\n# Check whether --enable-debug was given.\nif test \"${enable_debug+set}\" = set; then :\n  enableval=$enable_debug;\nfi\n\nif test \"x$enable_debug\" = \"xyes\"; then :\n  DEBUG=yes\n\nelse\n  DEBUG=no\n\nfi\n\nac_config_files=\"$ac_config_files Makefile contrib/pacman/PKGBUILD contrib/spec/onedrive.spec onedrive.1 contrib/systemd/onedrive.service contrib/systemd/onedrive@.service\"\n\ncat >confcache <<\\_ACEOF\n# This file is a shell script that caches the results of configure\n# tests run on this system so they can be shared between configure\n# scripts and configure runs, see configure's option --config-cache.\n# It is not useful on other systems.  If it contains results you don't\n# want to keep, you may remove or edit it.\n#\n# config.status only pays attention to the cache file if you give it\n# the --recheck option to rerun configure.\n#\n# `ac_cv_env_foo' variables (set or unset) will be overridden when\n# loading this file, other *unset* `ac_cv_foo' will be assigned the\n# following values.\n\n_ACEOF\n\n# The following way of writing the cache mishandles newlines in values,\n# but we know of no workaround that is simple, portable, and efficient.\n# So, we kill variables containing newlines.\n# Ultrix sh set writes to stderr and can't be redirected directly,\n# and sets the high bit in the cache file unless we assign to the vars.\n(\n  for ac_var in `(set) 2>&1 | sed -n 's/^\\([a-zA-Z_][a-zA-Z0-9_]*\\)=.*/\\1/p'`; do\n    eval ac_val=\\$$ac_var\n    case $ac_val in #(\n    *${as_nl}*)\n      case $ac_var in #(\n      *_cv_*) { $as_echo \"$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline\" >&5\n$as_echo \"$as_me: WARNING: cache variable $ac_var contains a newline\" >&2;} ;;\n      esac\n      case $ac_var in #(\n      _ | IFS | as_nl) ;; #(\n      BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #(\n      *) { eval $ac_var=; unset $ac_var;} ;;\n      esac ;;\n    esac\n  done\n\n  (set) 2>&1 |\n    case $as_nl`(ac_space=' '; set) 2>&1` in #(\n    *${as_nl}ac_space=\\ *)\n      # `set' does not quote correctly, so add quotes: double-quote\n      # substitution turns \\\\\\\\ into \\\\, and sed turns \\\\ into \\.\n      sed -n \\\n\t\"s/'/'\\\\\\\\''/g;\n\t  s/^\\\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\\\)=\\\\(.*\\\\)/\\\\1='\\\\2'/p\"\n      ;; #(\n    *)\n      # `set' quotes correctly as required by POSIX, so do not add quotes.\n      sed -n \"/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p\"\n      ;;\n    esac |\n    sort\n) |\n  sed '\n     /^ac_cv_env_/b end\n     t clear\n     :clear\n     s/^\\([^=]*\\)=\\(.*[{}].*\\)$/test \"${\\1+set}\" = set || &/\n     t end\n     s/^\\([^=]*\\)=\\(.*\\)$/\\1=${\\1=\\2}/\n     :end' >>confcache\nif diff \"$cache_file\" confcache >/dev/null 2>&1; then :; else\n  if test -w \"$cache_file\"; then\n    if test \"x$cache_file\" != \"x/dev/null\"; then\n      { $as_echo \"$as_me:${as_lineno-$LINENO}: updating cache $cache_file\" >&5\n$as_echo \"$as_me: updating cache $cache_file\" >&6;}\n      if test ! -f \"$cache_file\" || test -h \"$cache_file\"; then\n\tcat confcache >\"$cache_file\"\n      else\n        case $cache_file in #(\n        */* | ?:*)\n\t  mv -f confcache \"$cache_file\"$$ &&\n\t  mv -f \"$cache_file\"$$ \"$cache_file\" ;; #(\n        *)\n\t  mv -f confcache \"$cache_file\" ;;\n\tesac\n      fi\n    fi\n  else\n    { $as_echo \"$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file\" >&5\n$as_echo \"$as_me: not updating unwritable cache $cache_file\" >&6;}\n  fi\nfi\nrm -f confcache\n\ntest \"x$prefix\" = xNONE && prefix=$ac_default_prefix\n# Let make expand exec_prefix.\ntest \"x$exec_prefix\" = xNONE && exec_prefix='${prefix}'\n\n# Transform confdefs.h into DEFS.\n# Protect against shell expansion while executing Makefile rules.\n# Protect against Makefile macro expansion.\n#\n# If the first sed substitution is executed (which looks for macros that\n# take arguments), then branch to the quote section.  Otherwise,\n# look for a macro that doesn't take arguments.\nac_script='\n:mline\n/\\\\$/{\n N\n s,\\\\\\n,,\n b mline\n}\nt clear\n:clear\ns/^[\t ]*#[\t ]*define[\t ][\t ]*\\([^\t (][^\t (]*([^)]*)\\)[\t ]*\\(.*\\)/-D\\1=\\2/g\nt quote\ns/^[\t ]*#[\t ]*define[\t ][\t ]*\\([^\t ][^\t ]*\\)[\t ]*\\(.*\\)/-D\\1=\\2/g\nt quote\nb any\n:quote\ns/[\t `~#$^&*(){}\\\\|;'\\''\"<>?]/\\\\&/g\ns/\\[/\\\\&/g\ns/\\]/\\\\&/g\ns/\\$/$$/g\nH\n:any\n${\n\tg\n\ts/^\\n//\n\ts/\\n/ /g\n\tp\n}\n'\nDEFS=`sed -n \"$ac_script\" confdefs.h`\n\n\nac_libobjs=\nac_ltlibobjs=\nU=\nfor ac_i in : $LIBOBJS; do test \"x$ac_i\" = x: && continue\n  # 1. Remove the extension, and $U if already installed.\n  ac_script='s/\\$U\\././;s/\\.o$//;s/\\.obj$//'\n  ac_i=`$as_echo \"$ac_i\" | sed \"$ac_script\"`\n  # 2. Prepend LIBOBJDIR.  When used with automake>=1.10 LIBOBJDIR\n  #    will be set to the directory where LIBOBJS objects are built.\n  as_fn_append ac_libobjs \" \\${LIBOBJDIR}$ac_i\\$U.$ac_objext\"\n  as_fn_append ac_ltlibobjs \" \\${LIBOBJDIR}$ac_i\"'$U.lo'\ndone\nLIBOBJS=$ac_libobjs\n\nLTLIBOBJS=$ac_ltlibobjs\n\n\n\n: \"${CONFIG_STATUS=./config.status}\"\nac_write_fail=0\nac_clean_files_save=$ac_clean_files\nac_clean_files=\"$ac_clean_files $CONFIG_STATUS\"\n{ $as_echo \"$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS\" >&5\n$as_echo \"$as_me: creating $CONFIG_STATUS\" >&6;}\nas_write_fail=0\ncat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1\n#! $SHELL\n# Generated by $as_me.\n# Run this file to recreate the current configuration.\n# Compiler output produced by configure, useful for debugging\n# configure, is in config.log if it exists.\n\ndebug=false\nac_cs_recheck=false\nac_cs_silent=false\n\nSHELL=\\${CONFIG_SHELL-$SHELL}\nexport SHELL\n_ASEOF\ncat >>$CONFIG_STATUS <<\\_ASEOF || as_write_fail=1\n## -------------------- ##\n## M4sh Initialization. ##\n## -------------------- ##\n\n# Be more Bourne compatible\nDUALCASE=1; export DUALCASE # for MKS sh\nif test -n \"${ZSH_VERSION+set}\" && (emulate sh) >/dev/null 2>&1; then :\n  emulate sh\n  NULLCMD=:\n  # Pre-4.2 versions of Zsh do word splitting on ${1+\"$@\"}, which\n  # is contrary to our usage.  Disable this feature.\n  alias -g '${1+\"$@\"}'='\"$@\"'\n  setopt NO_GLOB_SUBST\nelse\n  case `(set -o) 2>/dev/null` in #(\n  *posix*) :\n    set -o posix ;; #(\n  *) :\n     ;;\nesac\nfi\n\n\nas_nl='\n'\nexport as_nl\n# Printing a long string crashes Solaris 7 /usr/bin/printf.\nas_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'\nas_echo=$as_echo$as_echo$as_echo$as_echo$as_echo\nas_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo\n# Prefer a ksh shell builtin over an external printf program on Solaris,\n# but without wasting forks for bash or zsh.\nif test -z \"$BASH_VERSION$ZSH_VERSION\" \\\n    && (test \"X`print -r -- $as_echo`\" = \"X$as_echo\") 2>/dev/null; then\n  as_echo='print -r --'\n  as_echo_n='print -rn --'\nelif (test \"X`printf %s $as_echo`\" = \"X$as_echo\") 2>/dev/null; then\n  as_echo='printf %s\\n'\n  as_echo_n='printf %s'\nelse\n  if test \"X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`\" = \"X-n $as_echo\"; then\n    as_echo_body='eval /usr/ucb/echo -n \"$1$as_nl\"'\n    as_echo_n='/usr/ucb/echo -n'\n  else\n    as_echo_body='eval expr \"X$1\" : \"X\\\\(.*\\\\)\"'\n    as_echo_n_body='eval\n      arg=$1;\n      case $arg in #(\n      *\"$as_nl\"*)\n\texpr \"X$arg\" : \"X\\\\(.*\\\\)$as_nl\";\n\targ=`expr \"X$arg\" : \".*$as_nl\\\\(.*\\\\)\"`;;\n      esac;\n      expr \"X$arg\" : \"X\\\\(.*\\\\)\" | tr -d \"$as_nl\"\n    '\n    export as_echo_n_body\n    as_echo_n='sh -c $as_echo_n_body as_echo'\n  fi\n  export as_echo_body\n  as_echo='sh -c $as_echo_body as_echo'\nfi\n\n# The user is always right.\nif test \"${PATH_SEPARATOR+set}\" != set; then\n  PATH_SEPARATOR=:\n  (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {\n    (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||\n      PATH_SEPARATOR=';'\n  }\nfi\n\n\n# IFS\n# We need space, tab and new line, in precisely that order.  Quoting is\n# there to prevent editors from complaining about space-tab.\n# (If _AS_PATH_WALK were called with IFS unset, it would disable word\n# splitting by setting IFS to empty value.)\nIFS=\" \"\"\t$as_nl\"\n\n# Find who we are.  Look in the path if we contain no directory separator.\nas_myself=\ncase $0 in #((\n  *[\\\\/]* ) as_myself=$0 ;;\n  *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\nfor as_dir in $PATH\ndo\n  IFS=$as_save_IFS\n  test -z \"$as_dir\" && as_dir=.\n    test -r \"$as_dir/$0\" && as_myself=$as_dir/$0 && break\n  done\nIFS=$as_save_IFS\n\n     ;;\nesac\n# We did not find ourselves, most probably we were run as `sh COMMAND'\n# in which case we are not to be found in the path.\nif test \"x$as_myself\" = x; then\n  as_myself=$0\nfi\nif test ! -f \"$as_myself\"; then\n  $as_echo \"$as_myself: error: cannot find myself; rerun with an absolute file name\" >&2\n  exit 1\nfi\n\n# Unset variables that we do not need and which cause bugs (e.g. in\n# pre-3.0 UWIN ksh).  But do not cause bugs in bash 2.01; the \"|| exit 1\"\n# suppresses any \"Segmentation fault\" message there.  '((' could\n# trigger a bug in pdksh 5.2.14.\nfor as_var in BASH_ENV ENV MAIL MAILPATH\ndo eval test x\\${$as_var+set} = xset \\\n  && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || :\ndone\nPS1='$ '\nPS2='> '\nPS4='+ '\n\n# NLS nuisances.\nLC_ALL=C\nexport LC_ALL\nLANGUAGE=C\nexport LANGUAGE\n\n# CDPATH.\n(unset CDPATH) >/dev/null 2>&1 && unset CDPATH\n\n\n# as_fn_error STATUS ERROR [LINENO LOG_FD]\n# ----------------------------------------\n# Output \"`basename $0`: error: ERROR\" to stderr. If LINENO and LOG_FD are\n# provided, also output the error to LOG_FD, referencing LINENO. Then exit the\n# script with STATUS, using 1 if that was 0.\nas_fn_error ()\n{\n  as_status=$1; test $as_status -eq 0 && as_status=1\n  if test \"$4\"; then\n    as_lineno=${as_lineno-\"$3\"} as_lineno_stack=as_lineno_stack=$as_lineno_stack\n    $as_echo \"$as_me:${as_lineno-$LINENO}: error: $2\" >&$4\n  fi\n  $as_echo \"$as_me: error: $2\" >&2\n  as_fn_exit $as_status\n} # as_fn_error\n\n\n# as_fn_set_status STATUS\n# -----------------------\n# Set $? to STATUS, without forking.\nas_fn_set_status ()\n{\n  return $1\n} # as_fn_set_status\n\n# as_fn_exit STATUS\n# -----------------\n# Exit the shell with STATUS, even in a \"trap 0\" or \"set -e\" context.\nas_fn_exit ()\n{\n  set +e\n  as_fn_set_status $1\n  exit $1\n} # as_fn_exit\n\n# as_fn_unset VAR\n# ---------------\n# Portably unset VAR.\nas_fn_unset ()\n{\n  { eval $1=; unset $1;}\n}\nas_unset=as_fn_unset\n# as_fn_append VAR VALUE\n# ----------------------\n# Append the text in VALUE to the end of the definition contained in VAR. Take\n# advantage of any shell optimizations that allow amortized linear growth over\n# repeated appends, instead of the typical quadratic growth present in naive\n# implementations.\nif (eval \"as_var=1; as_var+=2; test x\\$as_var = x12\") 2>/dev/null; then :\n  eval 'as_fn_append ()\n  {\n    eval $1+=\\$2\n  }'\nelse\n  as_fn_append ()\n  {\n    eval $1=\\$$1\\$2\n  }\nfi # as_fn_append\n\n# as_fn_arith ARG...\n# ------------------\n# Perform arithmetic evaluation on the ARGs, and store the result in the\n# global $as_val. Take advantage of shells that can avoid forks. The arguments\n# must be portable across $(()) and expr.\nif (eval \"test \\$(( 1 + 1 )) = 2\") 2>/dev/null; then :\n  eval 'as_fn_arith ()\n  {\n    as_val=$(( $* ))\n  }'\nelse\n  as_fn_arith ()\n  {\n    as_val=`expr \"$@\" || test $? -eq 1`\n  }\nfi # as_fn_arith\n\n\nif expr a : '\\(a\\)' >/dev/null 2>&1 &&\n   test \"X`expr 00001 : '.*\\(...\\)'`\" = X001; then\n  as_expr=expr\nelse\n  as_expr=false\nfi\n\nif (basename -- /) >/dev/null 2>&1 && test \"X`basename -- / 2>&1`\" = \"X/\"; then\n  as_basename=basename\nelse\n  as_basename=false\nfi\n\nif (as_dir=`dirname -- /` && test \"X$as_dir\" = X/) >/dev/null 2>&1; then\n  as_dirname=dirname\nelse\n  as_dirname=false\nfi\n\nas_me=`$as_basename -- \"$0\" ||\n$as_expr X/\"$0\" : '.*/\\([^/][^/]*\\)/*$' \\| \\\n\t X\"$0\" : 'X\\(//\\)$' \\| \\\n\t X\"$0\" : 'X\\(/\\)' \\| . 2>/dev/null ||\n$as_echo X/\"$0\" |\n    sed '/^.*\\/\\([^/][^/]*\\)\\/*$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\/\\(\\/\\/\\)$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\/\\(\\/\\).*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  s/.*/./; q'`\n\n# Avoid depending upon Character Ranges.\nas_cr_letters='abcdefghijklmnopqrstuvwxyz'\nas_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ'\nas_cr_Letters=$as_cr_letters$as_cr_LETTERS\nas_cr_digits='0123456789'\nas_cr_alnum=$as_cr_Letters$as_cr_digits\n\nECHO_C= ECHO_N= ECHO_T=\ncase `echo -n x` in #(((((\n-n*)\n  case `echo 'xy\\c'` in\n  *c*) ECHO_T='\t';;\t# ECHO_T is single tab character.\n  xy)  ECHO_C='\\c';;\n  *)   echo `echo ksh88 bug on AIX 6.1` > /dev/null\n       ECHO_T='\t';;\n  esac;;\n*)\n  ECHO_N='-n';;\nesac\n\nrm -f conf$$ conf$$.exe conf$$.file\nif test -d conf$$.dir; then\n  rm -f conf$$.dir/conf$$.file\nelse\n  rm -f conf$$.dir\n  mkdir conf$$.dir 2>/dev/null\nfi\nif (echo >conf$$.file) 2>/dev/null; then\n  if ln -s conf$$.file conf$$ 2>/dev/null; then\n    as_ln_s='ln -s'\n    # ... but there are two gotchas:\n    # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail.\n    # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable.\n    # In both cases, we have to default to `cp -pR'.\n    ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe ||\n      as_ln_s='cp -pR'\n  elif ln conf$$.file conf$$ 2>/dev/null; then\n    as_ln_s=ln\n  else\n    as_ln_s='cp -pR'\n  fi\nelse\n  as_ln_s='cp -pR'\nfi\nrm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file\nrmdir conf$$.dir 2>/dev/null\n\n\n# as_fn_mkdir_p\n# -------------\n# Create \"$as_dir\" as a directory, including parents if necessary.\nas_fn_mkdir_p ()\n{\n\n  case $as_dir in #(\n  -*) as_dir=./$as_dir;;\n  esac\n  test -d \"$as_dir\" || eval $as_mkdir_p || {\n    as_dirs=\n    while :; do\n      case $as_dir in #(\n      *\\'*) as_qdir=`$as_echo \"$as_dir\" | sed \"s/'/'\\\\\\\\\\\\\\\\''/g\"`;; #'(\n      *) as_qdir=$as_dir;;\n      esac\n      as_dirs=\"'$as_qdir' $as_dirs\"\n      as_dir=`$as_dirname -- \"$as_dir\" ||\n$as_expr X\"$as_dir\" : 'X\\(.*[^/]\\)//*[^/][^/]*/*$' \\| \\\n\t X\"$as_dir\" : 'X\\(//\\)[^/]' \\| \\\n\t X\"$as_dir\" : 'X\\(//\\)$' \\| \\\n\t X\"$as_dir\" : 'X\\(/\\)' \\| . 2>/dev/null ||\n$as_echo X\"$as_dir\" |\n    sed '/^X\\(.*[^/]\\)\\/\\/*[^/][^/]*\\/*$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\/\\)[^/].*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\/\\)$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\).*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  s/.*/./; q'`\n      test -d \"$as_dir\" && break\n    done\n    test -z \"$as_dirs\" || eval \"mkdir $as_dirs\"\n  } || test -d \"$as_dir\" || as_fn_error $? \"cannot create directory $as_dir\"\n\n\n} # as_fn_mkdir_p\nif mkdir -p . 2>/dev/null; then\n  as_mkdir_p='mkdir -p \"$as_dir\"'\nelse\n  test -d ./-p && rmdir ./-p\n  as_mkdir_p=false\nfi\n\n\n# as_fn_executable_p FILE\n# -----------------------\n# Test if FILE is an executable regular file.\nas_fn_executable_p ()\n{\n  test -f \"$1\" && test -x \"$1\"\n} # as_fn_executable_p\nas_test_x='test -x'\nas_executable_p=as_fn_executable_p\n\n# Sed expression to map a string onto a valid CPP name.\nas_tr_cpp=\"eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'\"\n\n# Sed expression to map a string onto a valid variable name.\nas_tr_sh=\"eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'\"\n\n\nexec 6>&1\n## ----------------------------------- ##\n## Main body of $CONFIG_STATUS script. ##\n## ----------------------------------- ##\n_ASEOF\ntest $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1\n\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\n# Save the log message, to keep $0 and so on meaningful, and to\n# report actual input values of CONFIG_FILES etc. instead of their\n# values after options handling.\nac_log=\"\nThis file was extended by onedrive $as_me v2.5.10, which was\ngenerated by GNU Autoconf 2.69.  Invocation command line was\n\n  CONFIG_FILES    = $CONFIG_FILES\n  CONFIG_HEADERS  = $CONFIG_HEADERS\n  CONFIG_LINKS    = $CONFIG_LINKS\n  CONFIG_COMMANDS = $CONFIG_COMMANDS\n  $ $0 $@\n\non `(hostname || uname -n) 2>/dev/null | sed 1q`\n\"\n\n_ACEOF\n\ncase $ac_config_files in *\"\n\"*) set x $ac_config_files; shift; ac_config_files=$*;;\nesac\n\n\n\ncat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\n# Files that config.status was made for.\nconfig_files=\"$ac_config_files\"\n\n_ACEOF\n\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\nac_cs_usage=\"\\\n\\`$as_me' instantiates files and other configuration actions\nfrom templates according to the current configuration.  Unless the files\nand actions are specified as TAGs, all are instantiated by default.\n\nUsage: $0 [OPTION]... [TAG]...\n\n  -h, --help       print this help, then exit\n  -V, --version    print version number and configuration settings, then exit\n      --config     print configuration, then exit\n  -q, --quiet, --silent\n                   do not print progress messages\n  -d, --debug      don't remove temporary files\n      --recheck    update $as_me by reconfiguring in the same conditions\n      --file=FILE[:TEMPLATE]\n                   instantiate the configuration file FILE\n\nConfiguration files:\n$config_files\n\nReport bugs to <https://github.com/abraunegg/onedrive>.\"\n\n_ACEOF\ncat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\nac_cs_config=\"`$as_echo \"$ac_configure_args\" | sed 's/^ //; s/[\\\\\"\"\\`\\$]/\\\\\\\\&/g'`\"\nac_cs_version=\"\\\\\nonedrive config.status v2.5.10\nconfigured by $0, generated by GNU Autoconf 2.69,\n  with options \\\\\"\\$ac_cs_config\\\\\"\n\nCopyright (C) 2012 Free Software Foundation, Inc.\nThis config.status script is free software; the Free Software Foundation\ngives unlimited permission to copy, distribute and modify it.\"\n\nac_pwd='$ac_pwd'\nsrcdir='$srcdir'\nINSTALL='$INSTALL'\ntest -n \"\\$AWK\" || AWK=awk\n_ACEOF\n\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\n# The default lists apply if the user does not specify any file.\nac_need_defaults=:\nwhile test $# != 0\ndo\n  case $1 in\n  --*=?*)\n    ac_option=`expr \"X$1\" : 'X\\([^=]*\\)='`\n    ac_optarg=`expr \"X$1\" : 'X[^=]*=\\(.*\\)'`\n    ac_shift=:\n    ;;\n  --*=)\n    ac_option=`expr \"X$1\" : 'X\\([^=]*\\)='`\n    ac_optarg=\n    ac_shift=:\n    ;;\n  *)\n    ac_option=$1\n    ac_optarg=$2\n    ac_shift=shift\n    ;;\n  esac\n\n  case $ac_option in\n  # Handling of the options.\n  -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r)\n    ac_cs_recheck=: ;;\n  --version | --versio | --versi | --vers | --ver | --ve | --v | -V )\n    $as_echo \"$ac_cs_version\"; exit ;;\n  --config | --confi | --conf | --con | --co | --c )\n    $as_echo \"$ac_cs_config\"; exit ;;\n  --debug | --debu | --deb | --de | --d | -d )\n    debug=: ;;\n  --file | --fil | --fi | --f )\n    $ac_shift\n    case $ac_optarg in\n    *\\'*) ac_optarg=`$as_echo \"$ac_optarg\" | sed \"s/'/'\\\\\\\\\\\\\\\\''/g\"` ;;\n    '') as_fn_error $? \"missing file argument\" ;;\n    esac\n    as_fn_append CONFIG_FILES \" '$ac_optarg'\"\n    ac_need_defaults=false;;\n  --he | --h |  --help | --hel | -h )\n    $as_echo \"$ac_cs_usage\"; exit ;;\n  -q | -quiet | --quiet | --quie | --qui | --qu | --q \\\n  | -silent | --silent | --silen | --sile | --sil | --si | --s)\n    ac_cs_silent=: ;;\n\n  # This is an error.\n  -*) as_fn_error $? \"unrecognized option: \\`$1'\nTry \\`$0 --help' for more information.\" ;;\n\n  *) as_fn_append ac_config_targets \" $1\"\n     ac_need_defaults=false ;;\n\n  esac\n  shift\ndone\n\nac_configure_extra_args=\n\nif $ac_cs_silent; then\n  exec 6>/dev/null\n  ac_configure_extra_args=\"$ac_configure_extra_args --silent\"\nfi\n\n_ACEOF\ncat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\nif \\$ac_cs_recheck; then\n  set X $SHELL '$0' $ac_configure_args \\$ac_configure_extra_args --no-create --no-recursion\n  shift\n  \\$as_echo \"running CONFIG_SHELL=$SHELL \\$*\" >&6\n  CONFIG_SHELL='$SHELL'\n  export CONFIG_SHELL\n  exec \"\\$@\"\nfi\n\n_ACEOF\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\nexec 5>>config.log\n{\n  echo\n  sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX\n## Running $as_me. ##\n_ASBOX\n  $as_echo \"$ac_log\"\n} >&5\n\n_ACEOF\ncat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\n_ACEOF\n\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\n\n# Handling of arguments.\nfor ac_config_target in $ac_config_targets\ndo\n  case $ac_config_target in\n    \"Makefile\") CONFIG_FILES=\"$CONFIG_FILES Makefile\" ;;\n    \"contrib/pacman/PKGBUILD\") CONFIG_FILES=\"$CONFIG_FILES contrib/pacman/PKGBUILD\" ;;\n    \"contrib/spec/onedrive.spec\") CONFIG_FILES=\"$CONFIG_FILES contrib/spec/onedrive.spec\" ;;\n    \"onedrive.1\") CONFIG_FILES=\"$CONFIG_FILES onedrive.1\" ;;\n    \"contrib/systemd/onedrive.service\") CONFIG_FILES=\"$CONFIG_FILES contrib/systemd/onedrive.service\" ;;\n    \"contrib/systemd/onedrive@.service\") CONFIG_FILES=\"$CONFIG_FILES contrib/systemd/onedrive@.service\" ;;\n\n  *) as_fn_error $? \"invalid argument: \\`$ac_config_target'\" \"$LINENO\" 5;;\n  esac\ndone\n\n\n# If the user did not use the arguments to specify the items to instantiate,\n# then the envvar interface is used.  Set only those that are not.\n# We use the long form for the default assignment because of an extremely\n# bizarre bug on SunOS 4.1.3.\nif $ac_need_defaults; then\n  test \"${CONFIG_FILES+set}\" = set || CONFIG_FILES=$config_files\nfi\n\n# Have a temporary directory for convenience.  Make it in the build tree\n# simply because there is no reason against having it here, and in addition,\n# creating and moving files from /tmp can sometimes cause problems.\n# Hook for its removal unless debugging.\n# Note that there is a small window in which the directory will not be cleaned:\n# after its creation but before its name has been assigned to `$tmp'.\n$debug ||\n{\n  tmp= ac_tmp=\n  trap 'exit_status=$?\n  : \"${ac_tmp:=$tmp}\"\n  { test ! -d \"$ac_tmp\" || rm -fr \"$ac_tmp\"; } && exit $exit_status\n' 0\n  trap 'as_fn_exit 1' 1 2 13 15\n}\n# Create a (secure) tmp directory for tmp files.\n\n{\n  tmp=`(umask 077 && mktemp -d \"./confXXXXXX\") 2>/dev/null` &&\n  test -d \"$tmp\"\n}  ||\n{\n  tmp=./conf$$-$RANDOM\n  (umask 077 && mkdir \"$tmp\")\n} || as_fn_error $? \"cannot create a temporary directory in .\" \"$LINENO\" 5\nac_tmp=$tmp\n\n# Set up the scripts for CONFIG_FILES section.\n# No need to generate them if there are no CONFIG_FILES.\n# This happens for instance with `./config.status config.h'.\nif test -n \"$CONFIG_FILES\"; then\n\n\nac_cr=`echo X | tr X '\\015'`\n# On cygwin, bash can eat \\r inside `` if the user requested igncr.\n# But we know of no other shell where ac_cr would be empty at this\n# point, so we can use a bashism as a fallback.\nif test \"x$ac_cr\" = x; then\n  eval ac_cr=\\$\\'\\\\r\\'\nfi\nac_cs_awk_cr=`$AWK 'BEGIN { print \"a\\rb\" }' </dev/null 2>/dev/null`\nif test \"$ac_cs_awk_cr\" = \"a${ac_cr}b\"; then\n  ac_cs_awk_cr='\\\\r'\nelse\n  ac_cs_awk_cr=$ac_cr\nfi\n\necho 'BEGIN {' >\"$ac_tmp/subs1.awk\" &&\n_ACEOF\n\n\n{\n  echo \"cat >conf$$subs.awk <<_ACEOF\" &&\n  echo \"$ac_subst_vars\" | sed 's/.*/&!$&$ac_delim/' &&\n  echo \"_ACEOF\"\n} >conf$$subs.sh ||\n  as_fn_error $? \"could not make $CONFIG_STATUS\" \"$LINENO\" 5\nac_delim_num=`echo \"$ac_subst_vars\" | grep -c '^'`\nac_delim='%!_!# '\nfor ac_last_try in false false false false false :; do\n  . ./conf$$subs.sh ||\n    as_fn_error $? \"could not make $CONFIG_STATUS\" \"$LINENO\" 5\n\n  ac_delim_n=`sed -n \"s/.*$ac_delim\\$/X/p\" conf$$subs.awk | grep -c X`\n  if test $ac_delim_n = $ac_delim_num; then\n    break\n  elif $ac_last_try; then\n    as_fn_error $? \"could not make $CONFIG_STATUS\" \"$LINENO\" 5\n  else\n    ac_delim=\"$ac_delim!$ac_delim _$ac_delim!! \"\n  fi\ndone\nrm -f conf$$subs.sh\n\ncat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\ncat >>\"\\$ac_tmp/subs1.awk\" <<\\\\_ACAWK &&\n_ACEOF\nsed -n '\nh\ns/^/S[\"/; s/!.*/\"]=/\np\ng\ns/^[^!]*!//\n:repl\nt repl\ns/'\"$ac_delim\"'$//\nt delim\n:nl\nh\ns/\\(.\\{148\\}\\)..*/\\1/\nt more1\ns/[\"\\\\]/\\\\&/g; s/^/\"/; s/$/\\\\n\"\\\\/\np\nn\nb repl\n:more1\ns/[\"\\\\]/\\\\&/g; s/^/\"/; s/$/\"\\\\/\np\ng\ns/.\\{148\\}//\nt nl\n:delim\nh\ns/\\(.\\{148\\}\\)..*/\\1/\nt more2\ns/[\"\\\\]/\\\\&/g; s/^/\"/; s/$/\"/\np\nb\n:more2\ns/[\"\\\\]/\\\\&/g; s/^/\"/; s/$/\"\\\\/\np\ng\ns/.\\{148\\}//\nt delim\n' <conf$$subs.awk | sed '\n/^[^\"\"]/{\n  N\n  s/\\n//\n}\n' >>$CONFIG_STATUS || ac_write_fail=1\nrm -f conf$$subs.awk\ncat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\n_ACAWK\ncat >>\"\\$ac_tmp/subs1.awk\" <<_ACAWK &&\n  for (key in S) S_is_set[key] = 1\n  FS = \"\u0007\"\n\n}\n{\n  line = $ 0\n  nfields = split(line, field, \"@\")\n  substed = 0\n  len = length(field[1])\n  for (i = 2; i < nfields; i++) {\n    key = field[i]\n    keylen = length(key)\n    if (S_is_set[key]) {\n      value = S[key]\n      line = substr(line, 1, len) \"\" value \"\" substr(line, len + keylen + 3)\n      len += length(value) + length(field[++i])\n      substed = 1\n    } else\n      len += 1 + keylen\n  }\n\n  print line\n}\n\n_ACAWK\n_ACEOF\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\nif sed \"s/$ac_cr//\" < /dev/null > /dev/null 2>&1; then\n  sed \"s/$ac_cr\\$//; s/$ac_cr/$ac_cs_awk_cr/g\"\nelse\n  cat\nfi < \"$ac_tmp/subs1.awk\" > \"$ac_tmp/subs.awk\" \\\n  || as_fn_error $? \"could not setup config files machinery\" \"$LINENO\" 5\n_ACEOF\n\n# VPATH may cause trouble with some makes, so we remove sole $(srcdir),\n# ${srcdir} and @srcdir@ entries from VPATH if srcdir is \".\", strip leading and\n# trailing colons and then remove the whole line if VPATH becomes empty\n# (actually we leave an empty line to preserve line numbers).\nif test \"x$srcdir\" = x.; then\n  ac_vpsub='/^[\t ]*VPATH[\t ]*=[\t ]*/{\nh\ns///\ns/^/:/\ns/[\t ]*$/:/\ns/:\\$(srcdir):/:/g\ns/:\\${srcdir}:/:/g\ns/:@srcdir@:/:/g\ns/^:*//\ns/:*$//\nx\ns/\\(=[\t ]*\\).*/\\1/\nG\ns/\\n//\ns/^[^=]*=[\t ]*$//\n}'\nfi\n\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\nfi # test -n \"$CONFIG_FILES\"\n\n\neval set X \"  :F $CONFIG_FILES      \"\nshift\nfor ac_tag\ndo\n  case $ac_tag in\n  :[FHLC]) ac_mode=$ac_tag; continue;;\n  esac\n  case $ac_mode$ac_tag in\n  :[FHL]*:*);;\n  :L* | :C*:*) as_fn_error $? \"invalid tag \\`$ac_tag'\" \"$LINENO\" 5;;\n  :[FH]-) ac_tag=-:-;;\n  :[FH]*) ac_tag=$ac_tag:$ac_tag.in;;\n  esac\n  ac_save_IFS=$IFS\n  IFS=:\n  set x $ac_tag\n  IFS=$ac_save_IFS\n  shift\n  ac_file=$1\n  shift\n\n  case $ac_mode in\n  :L) ac_source=$1;;\n  :[FH])\n    ac_file_inputs=\n    for ac_f\n    do\n      case $ac_f in\n      -) ac_f=\"$ac_tmp/stdin\";;\n      *) # Look for the file first in the build tree, then in the source tree\n\t # (if the path is not absolute).  The absolute path cannot be DOS-style,\n\t # because $ac_f cannot contain `:'.\n\t test -f \"$ac_f\" ||\n\t   case $ac_f in\n\t   [\\\\/$]*) false;;\n\t   *) test -f \"$srcdir/$ac_f\" && ac_f=\"$srcdir/$ac_f\";;\n\t   esac ||\n\t   as_fn_error 1 \"cannot find input file: \\`$ac_f'\" \"$LINENO\" 5;;\n      esac\n      case $ac_f in *\\'*) ac_f=`$as_echo \"$ac_f\" | sed \"s/'/'\\\\\\\\\\\\\\\\''/g\"`;; esac\n      as_fn_append ac_file_inputs \" '$ac_f'\"\n    done\n\n    # Let's still pretend it is `configure' which instantiates (i.e., don't\n    # use $as_me), people would be surprised to read:\n    #    /* config.h.  Generated by config.status.  */\n    configure_input='Generated from '`\n\t  $as_echo \"$*\" | sed 's|^[^:]*/||;s|:[^:]*/|, |g'\n\t`' by configure.'\n    if test x\"$ac_file\" != x-; then\n      configure_input=\"$ac_file.  $configure_input\"\n      { $as_echo \"$as_me:${as_lineno-$LINENO}: creating $ac_file\" >&5\n$as_echo \"$as_me: creating $ac_file\" >&6;}\n    fi\n    # Neutralize special characters interpreted by sed in replacement strings.\n    case $configure_input in #(\n    *\\&* | *\\|* | *\\\\* )\n       ac_sed_conf_input=`$as_echo \"$configure_input\" |\n       sed 's/[\\\\\\\\&|]/\\\\\\\\&/g'`;; #(\n    *) ac_sed_conf_input=$configure_input;;\n    esac\n\n    case $ac_tag in\n    *:-:* | *:-) cat >\"$ac_tmp/stdin\" \\\n      || as_fn_error $? \"could not create $ac_file\" \"$LINENO\" 5 ;;\n    esac\n    ;;\n  esac\n\n  ac_dir=`$as_dirname -- \"$ac_file\" ||\n$as_expr X\"$ac_file\" : 'X\\(.*[^/]\\)//*[^/][^/]*/*$' \\| \\\n\t X\"$ac_file\" : 'X\\(//\\)[^/]' \\| \\\n\t X\"$ac_file\" : 'X\\(//\\)$' \\| \\\n\t X\"$ac_file\" : 'X\\(/\\)' \\| . 2>/dev/null ||\n$as_echo X\"$ac_file\" |\n    sed '/^X\\(.*[^/]\\)\\/\\/*[^/][^/]*\\/*$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\/\\)[^/].*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\/\\)$/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  /^X\\(\\/\\).*/{\n\t    s//\\1/\n\t    q\n\t  }\n\t  s/.*/./; q'`\n  as_dir=\"$ac_dir\"; as_fn_mkdir_p\n  ac_builddir=.\n\ncase \"$ac_dir\" in\n.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;;\n*)\n  ac_dir_suffix=/`$as_echo \"$ac_dir\" | sed 's|^\\.[\\\\/]||'`\n  # A \"..\" for each directory in $ac_dir_suffix.\n  ac_top_builddir_sub=`$as_echo \"$ac_dir_suffix\" | sed 's|/[^\\\\/]*|/..|g;s|/||'`\n  case $ac_top_builddir_sub in\n  \"\") ac_top_builddir_sub=. ac_top_build_prefix= ;;\n  *)  ac_top_build_prefix=$ac_top_builddir_sub/ ;;\n  esac ;;\nesac\nac_abs_top_builddir=$ac_pwd\nac_abs_builddir=$ac_pwd$ac_dir_suffix\n# for backward compatibility:\nac_top_builddir=$ac_top_build_prefix\n\ncase $srcdir in\n  .)  # We are building in place.\n    ac_srcdir=.\n    ac_top_srcdir=$ac_top_builddir_sub\n    ac_abs_top_srcdir=$ac_pwd ;;\n  [\\\\/]* | ?:[\\\\/]* )  # Absolute name.\n    ac_srcdir=$srcdir$ac_dir_suffix;\n    ac_top_srcdir=$srcdir\n    ac_abs_top_srcdir=$srcdir ;;\n  *) # Relative name.\n    ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix\n    ac_top_srcdir=$ac_top_build_prefix$srcdir\n    ac_abs_top_srcdir=$ac_pwd/$srcdir ;;\nesac\nac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix\n\n\n  case $ac_mode in\n  :F)\n  #\n  # CONFIG_FILE\n  #\n\n  case $INSTALL in\n  [\\\\/$]* | ?:[\\\\/]* ) ac_INSTALL=$INSTALL ;;\n  *) ac_INSTALL=$ac_top_build_prefix$INSTALL ;;\n  esac\n_ACEOF\n\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\n# If the template does not know about datarootdir, expand it.\n# FIXME: This hack should be removed a few years after 2.60.\nac_datarootdir_hack=; ac_datarootdir_seen=\nac_sed_dataroot='\n/datarootdir/ {\n  p\n  q\n}\n/@datadir@/p\n/@docdir@/p\n/@infodir@/p\n/@localedir@/p\n/@mandir@/p'\ncase `eval \"sed -n \\\"\\$ac_sed_dataroot\\\" $ac_file_inputs\"` in\n*datarootdir*) ac_datarootdir_seen=yes;;\n*@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*)\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting\" >&5\n$as_echo \"$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting\" >&2;}\n_ACEOF\ncat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\n  ac_datarootdir_hack='\n  s&@datadir@&$datadir&g\n  s&@docdir@&$docdir&g\n  s&@infodir@&$infodir&g\n  s&@localedir@&$localedir&g\n  s&@mandir@&$mandir&g\n  s&\\\\\\${datarootdir}&$datarootdir&g' ;;\nesac\n_ACEOF\n\n# Neutralize VPATH when `$srcdir' = `.'.\n# Shell code in configure.ac might set extrasub.\n# FIXME: do we really want to maintain this feature?\ncat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\nac_sed_extra=\"$ac_vpsub\n$extrasub\n_ACEOF\ncat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\n:t\n/@[a-zA-Z_][a-zA-Z_0-9]*@/!b\ns|@configure_input@|$ac_sed_conf_input|;t t\ns&@top_builddir@&$ac_top_builddir_sub&;t t\ns&@top_build_prefix@&$ac_top_build_prefix&;t t\ns&@srcdir@&$ac_srcdir&;t t\ns&@abs_srcdir@&$ac_abs_srcdir&;t t\ns&@top_srcdir@&$ac_top_srcdir&;t t\ns&@abs_top_srcdir@&$ac_abs_top_srcdir&;t t\ns&@builddir@&$ac_builddir&;t t\ns&@abs_builddir@&$ac_abs_builddir&;t t\ns&@abs_top_builddir@&$ac_abs_top_builddir&;t t\ns&@INSTALL@&$ac_INSTALL&;t t\n$ac_datarootdir_hack\n\"\neval sed \\\"\\$ac_sed_extra\\\" \"$ac_file_inputs\" | $AWK -f \"$ac_tmp/subs.awk\" \\\n  >$ac_tmp/out || as_fn_error $? \"could not create $ac_file\" \"$LINENO\" 5\n\ntest -z \"$ac_datarootdir_hack$ac_datarootdir_seen\" &&\n  { ac_out=`sed -n '/\\${datarootdir}/p' \"$ac_tmp/out\"`; test -n \"$ac_out\"; } &&\n  { ac_out=`sed -n '/^[\t ]*datarootdir[\t ]*:*=/p' \\\n      \"$ac_tmp/out\"`; test -z \"$ac_out\"; } &&\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \\`datarootdir'\nwhich seems to be undefined.  Please make sure it is defined\" >&5\n$as_echo \"$as_me: WARNING: $ac_file contains a reference to the variable \\`datarootdir'\nwhich seems to be undefined.  Please make sure it is defined\" >&2;}\n\n  rm -f \"$ac_tmp/stdin\"\n  case $ac_file in\n  -) cat \"$ac_tmp/out\" && rm -f \"$ac_tmp/out\";;\n  *) rm -f \"$ac_file\" && mv \"$ac_tmp/out\" \"$ac_file\";;\n  esac \\\n  || as_fn_error $? \"could not create $ac_file\" \"$LINENO\" 5\n ;;\n\n\n\n  esac\n\ndone # for ac_tag\n\n\nas_fn_exit 0\n_ACEOF\nac_clean_files=$ac_clean_files_save\n\ntest $ac_write_fail = 0 ||\n  as_fn_error $? \"write failure creating $CONFIG_STATUS\" \"$LINENO\" 5\n\n\n# configure is writing to config.log, and then calls config.status.\n# config.status does its own redirection, appending to config.log.\n# Unfortunately, on DOS this fails, as config.log is still kept open\n# by configure, so config.status won't be able to write to it; its\n# output is simply discarded.  So we exec the FD to /dev/null,\n# effectively closing config.log, so it can be properly (re)opened and\n# appended to by config.status.  When coming back to configure, we\n# need to make the FD available again.\nif test \"$no_create\" != yes; then\n  ac_cs_success=:\n  ac_config_status_args=\n  test \"$silent\" = yes &&\n    ac_config_status_args=\"$ac_config_status_args --quiet\"\n  exec 5>/dev/null\n  $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false\n  exec 5>>config.log\n  # Use ||, not &&, to avoid exiting from the if with $? = 1, which\n  # would make configure fail if this is the last instruction.\n  $ac_cs_success || as_fn_exit 1\nfi\nif test -n \"$ac_unrecognized_opts\" && test \"$enable_option_checking\" != no; then\n  { $as_echo \"$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts\" >&5\n$as_echo \"$as_me: WARNING: unrecognized options: $ac_unrecognized_opts\" >&2;}\nfi\n\n"
  },
  {
    "path": "configure.ac",
    "content": "dnl configure.ac for OneDrive Linux Client\ndnl Copyright 2019 Norbert Preining\ndnl Licensed GPL v3 or later\n\ndnl How to make a release\ndnl - increase the version number in the AC_INIT call below\ndnl - change PACKAGE_DATE to 'Month YYYY' to ensure man page has the correct date\ndnl - run autoconf which generates configure\ndnl - commit the changed files (configure.ac, configure)\ndnl - tag the release\n\nAC_PREREQ([2.69])\nAC_INIT([onedrive],[v2.5.10], [https://github.com/abraunegg/onedrive], [onedrive])\nAC_CONFIG_SRCDIR([src/main.d])\n\n\nAC_ARG_VAR([DC], [D compiler executable])\nAC_ARG_VAR([DCFLAGS], [flags for D compiler])\n\ndnl necessary programs: install, pkg-config\nAC_PROG_INSTALL\nPKG_PROG_PKG_CONFIG\n\nPACKAGE_DATE=\"January 2026\"\nAC_SUBST([PACKAGE_DATE])\n\ndnl Determine D compiler\ndnl we check for dmd, dmd2, and ldc2 in this order\ndnl furthermore, we set DC_TYPE to either dmd or ldc and export this into the\ndnl Makefile so that we can adjust command line arguments\nAC_CHECK_PROGS([DC], [dmd ldmd2 ldc2 gdmd gdc], NOT_FOUND)\nDC_TYPE=\ncase $(basename $DC) in\n        *ldc2*) DC_TYPE=ldc ;;\n        *gdc*) DC_TYPE=gdc ;;\n        *dmd*) DC_TYPE=dmd ;;\n        NOT_FOUND) AC_MSG_ERROR(Could not find any compatible D compiler, 1)\nesac\n\ndnl dash/POSIX version of version comparison\nvercomp () {\n    IFS=. read -r a0 a1 a2 aa <<EOF\n$1\nEOF\n    IFS=. read -r b0 b1 b2 bb <<EOF\n$2\nEOF\n    # leading 0 are ignored: 01 == 1, this also\n    # converts empty strings into 0: 1..2 == 1.0.2\n    a0=$(expr $a0 + 0)\n    a1=$(expr $a1 + 0)\n    a2=$(expr $a2 + 0)\n    b0=$(expr $b0 + 0)\n    b1=$(expr $b1 + 0)\n    b2=$(expr $b2 + 0)\n    #echo \"$1 parsed to a=$a0 b=$a1 c=$a2 rest=$aa\"\n    #echo \"$2 parsed to a=$b0 b=$b1 c=$b2 rest=$bb\"\n    if test $a0 -lt $b0 \n    then\n      return 2\n    elif test $a0 -gt $b0 \n    then\n      return 1\n    else\n      if test $a1 -lt $b1\n      then\n        return 2\n      elif test $a1 -gt $b1\n      then\n        return 1\n      else\n        if test $a2 -lt $b2\n\tthen\n\t  return 2\n\telif test $a2 -gt $b2\n\tthen\n\t  return 1\n\telse\n\t  if test $aa '<' $bb\n\t  then\n\t    return 2\n\t  elif test $aa '>' $bb\n\t  then\n\t    return 1\n          else\n\t    return 0\n\t  fi\n\tfi\n      fi\n    fi\n}\n\nDO_VERSION_CHECK=1\nAC_ARG_ENABLE(version-check,\n  AS_HELP_STRING([--disable-version-check], [Disable checks of compiler version during configure time]))\nAS_IF([test \"x$enable_version_check\" = \"xno\"], DO_VERSION_CHECK=0,)\n\nAS_IF([test \"$DO_VERSION_CHECK\" = \"1\"],\n      [ dnl do the version check\nAC_MSG_CHECKING([version of D compiler])\n# check for valid versions\ncase $(basename $DC) in\n\t*ldmd2*|*ldc2*)\n\t\t# LDC - the LLVM D compiler (1.12.0): ...\n\t\tVERSION=`$DC --version`\n\t\t# remove  everything up to first (\n\t\tVERSION=${VERSION#* (}\n\t\t# remove everything after ):\n\t\tVERSION=${VERSION%%):*}\n\t\t# now version should be something like L.M.N\n\t\tMINVERSION=1.20.1\n\t\t;;\n\t*gdmd*|*gdc*)\n\t\t# Both gdmd and gdc print the same version information\n\t\tVERSION=`${DC} --version | head -n1`\n\t\t# Some examples of output:\n\t\t# gdc (Gentoo 14.2.1_p20250301 p8) 14.2.1 20250301\n\t\t# gcc (GCC) 14.2.1 20250207 # Arch\n\t\t# gdc (GCC) 14.2.1 20250110 (Red Hat 14.2.1-7)\n\t\t# gdc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0\n\t\tVERSION=${VERSION#gdc }\n\t\t# VERSION=(...) VER DATE ...\n\t\tVERSION=${VERSION#*) }\n\t\t# VERSION=VER DATE ...\n\t\tVERSION=${VERSION%% *}\n\t\tMINVERSION=15\n\t\t;;\n\t*dmd*)\n\t\t# DMD64 D Compiler v2.085.1\\n...\n\t\tVERSION=`$DC --version | tr '\\n' ' '`\n\t\tVERSION=${VERSION#*Compiler v}\n\t\tVERSION=${VERSION%% *}\n\t\t# now version should be something like L.M.N\n\t\tMINVERSION=2.091.1\n\t\t;;\nesac\n\nAC_MSG_RESULT([$VERSION])\n\nvercomp $MINVERSION $VERSION\nif test $? = 1\nthen\n\tAC_MSG_ERROR([Compiler version insufficient, current compiler version $VERSION, minimum version $MINVERSION], 1)\nfi\n#echo \"MINVERSION=$MINVERSION VERSION=$VERSION\"\n      ])\n\n\n\ndnl In case the environment variable DCFLAGS is set, we export it to the\ndnl generated Makefile at configure run:\nAC_SUBST([DCFLAGS])\n\ndnl Default flags for each compiler\ncase \"$DC_TYPE\" in\n\tdmd)\n\t\tDEBUG_DCFLAGS=\"-g -debug -gs\"\n\t\tRELEASE_DCFLAGS=-O\n\t\tVERSION_DCFLAG=-version\n\t\tLINKER_DCFLAG=-L\n\t\tOUTPUT_DCFLAG=-of\n\t\tWERROR_DCFLAG=-w\n\t\t;;\n\tldc)\n\t\tDEBUG_DCFLAGS=\"-g -d-debug -gc\"\n\t\tRELEASE_DCFLAGS=-O\n\t\tVERSION_DCFLAG=-d-version\n\t\tLINKER_DCFLAG=-L\n\t\tOUTPUT_DCFLAG=-of\n\t\tWERROR_DCFLAG=-w\n\t\t;;\n\tgdc)\n\t\tDEBUG_DCFLAGS=\"-g -fdebug\"\n\t\tRELEASE_DCFLAGS=-O\n\t\tVERSION_DCFLAG=-fversion\n\t\tLINKER_DCFLAG=-Wl,\n\t\tOUTPUT_DCFLAG=-o\n\t\tWERROR_DCFLAG=-Werror\n\t\t;;\nesac\nAC_SUBST([DEBUG_DCFLAGS])\nAC_SUBST([RELEASE_DCFLAGS])\nAC_SUBST([VERSION_DCFLAG])\nAC_SUBST([LINKER_DCFLAG])\nAC_SUBST([OUTPUT_DCFLAG])\nAC_SUBST([WERROR_DCFLAG])\n\ndnl Check for required modules: curl, sqlite and dbus if required\nPKG_CHECK_MODULES([curl],[libcurl])\nPKG_CHECK_MODULES([sqlite],[sqlite3])\n\nAC_MSG_CHECKING([whether to enable dbus support])\ncase \"$(uname -s)\" in\n  Linux)\n    enable_dbus=yes\n    AC_MSG_RESULT([yes (on Linux)])\n    PKG_CHECK_MODULES([dbus], [dbus-1 >= 1.0],\n      [AC_DEFINE([HAVE_DBUS], [1], [Define if you have dbus-1])]\n      ,\n      [AC_MSG_ERROR([dbus-1 development files not found. Please install dbus-devel (Red Hat), libdbus-1-dev (Debian) or dbus (Arch | Manjaro)])]\n    )\n    ;;\n  *)\n    enable_dbus=no\n    AC_MSG_RESULT([no (not on Linux)])\n    ;;\nesac\n\nAC_SUBST([enable_dbus])\n\n\n\ndnl\ndnl systemd and unit file directories\ndnl This is a bit tricky, because we want to allow for \ndnl   --with-systemdsystemunitdir=auto\ndnl as well as =/path/to/dir\ndnl The first step is that we check whether the --with options is passed to configure run\ndnl if yes, we don't do anything (the ,, at the end of the next line), and if not, we\ndnl set with_systemdsystemunitdir=auto, meaning we will try pkg-config to find the correct\ndnl value.\nAC_ARG_WITH([systemdsystemunitdir],\n     [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd system service files])],,\n     [with_systemdsystemunitdir=auto])\ndnl If no value is passed in (or auto/yes is passed in), then we try to find the correct\ndnl value via pkg-config and put it into $def_systemdsystemunitdir\nAS_IF([test \"x$with_systemdsystemunitdir\" = \"xyes\" -o \"x$with_systemdsystemunitdir\" = \"xauto\"],\n      [ dnl true part, so try to determine with pkg-config\n        def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)\n        dnl if we cannot find it via pkg-config, *and* the user explicitly passed it in with,\n        dnl we warn, and in all cases we unset (set to no) the respective variable\n        AS_IF([test \"x$def_systemdsystemunitdir\" = \"x\"],\n              [  dnl we couldn't find the default value via pkg-config\n                AS_IF([test \"x$with_systemdsystemunitdir\" = \"xyes\"],\n                      [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])])\n                with_systemdsystemunitdir=no\n              ],\n              [ dnl pkg-config found the value, use it\n                with_systemdsystemunitdir=\"$def_systemdsystemunitdir\"\n              ]\n        )\n      ]\n)\ndnl finally, if we found a value, put it into the generated Makefile\nAS_IF([test \"x$with_systemdsystemunitdir\" != \"xno\"],\n      [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])])\n\ndnl Now do the same as above for systemduserunitdir!\nAC_ARG_WITH([systemduserunitdir],\n     [AS_HELP_STRING([--with-systemduserunitdir=DIR], [Directory for systemd user service files])],,\n     [with_systemduserunitdir=auto])\nAS_IF([test \"x$with_systemduserunitdir\" = \"xyes\" -o \"x$with_systemduserunitdir\" = \"xauto\"],\n      [\n        def_systemduserunitdir=$($PKG_CONFIG --variable=systemduserunitdir systemd)\n        AS_IF([test \"x$def_systemduserunitdir\" = \"x\"],\n              [\n                AS_IF([test \"x$with_systemduserunitdir\" = \"xyes\"],\n                      [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])])\n                with_systemduserunitdir=no\n              ],\n              [\n                with_systemduserunitdir=\"$def_systemduserunitdir\"\n              ]\n        )\n      ]\n)\nAS_IF([test \"x$with_systemduserunitdir\" != \"xno\"],\n      [AC_SUBST([systemduserunitdir], [$with_systemduserunitdir])])\n\ndnl We enable systemd integration only if we have found both user/system unit dirs\nAS_IF([test \"x$with_systemduserunitdir\" != \"xno\" -a \"x$with_systemdsystemunitdir\" != \"xno\"],\n      [havesystemd=yes], [havesystemd=no])\nAC_SUBST([HAVE_SYSTEMD], $havesystemd)\n\n\n\ndnl\ndnl Notification support\ndnl only check for libnotify if --enable-notifications is given\nAC_ARG_ENABLE(notifications,\n  AS_HELP_STRING([--enable-notifications], [Enable desktop notifications via libnotify]))\n\nAS_IF([test \"x$enable_notifications\" = \"xyes\"], [enable_notifications=yes], [enable_notifications=no])\n\ndnl if --enable-notifications was given, check for libnotify, and disable if not found\ndnl otherwise substitute the notifu\nAS_IF([test \"x$enable_notifications\" = \"xyes\"],\n      [PKG_CHECK_MODULES(notify,libnotify,,enable_notifications=no)],\n      [AC_SUBST([notify_LIBS],\"\")])\nAC_SUBST([NOTIFICATIONS],$enable_notifications)\n\ndnl\ndnl iNotify Support\n\n# Conditionally set bsd_inotify_LIBS based on the platform\ncase \"$(uname -s)\" in\n    Linux)\n        bsd_inotify_LIBS=\"\"\n        ;;\n    FreeBSD)\n        AS_IF([test \"$(uname -U)\" -gt 1500060],\n            [bsd_inotify_LIBS=\"\"],\n            [bsd_inotify_LIBS=\"-L/usr/local/lib -linotify\"]\n        )\n        ;;\n    OpenBSD)\n        bsd_inotify_LIBS=\"-L/usr/local/lib/inotify -linotify\"\n        ;;\n    *)\n        bsd_inotify_LIBS=\"\"\n        ;;\nesac\n\nAC_SUBST([bsd_inotify_LIBS])\n\ndnl\ndnl Dynamic Linker Support\n\n# Conditionally set dynamic_linker_LIBS based on the platform\ncase \"$(uname -s)\" in\n    Linux)\n        dynamic_linker_LIBS=\"-ldl\"\n        ;;\n    *)\n        dynamic_linker_LIBS=\"\"\n        ;;\nesac\n\nAC_SUBST([dynamic_linker_LIBS])\n\n\ndnl\ndnl Completion support\ndnl First determine whether completions are requested, pass that to Makefile\nAC_ARG_ENABLE([completions],\n\t      AS_HELP_STRING([--enable-completions], [Install shell completions for bash, zsh, and fish]))\n\nAS_IF([test \"x$enable_completions\" = \"xyes\"], [enable_completions=yes], [enable_completions=no])\n\nAC_SUBST([COMPLETIONS],$enable_completions)\n\n\ndnl if completions are enabled, search for the bash/zsh completion directory in the\ndnl similar way as we did for the systemd directories\nAS_IF([test \"x$enable_completions\" = \"xyes\"],[\n  AC_ARG_WITH([bash-completion-dir],\n              [AS_HELP_STRING([--with-bash-completion-dir=DIR], [Directory for bash completion files])],\n              ,\n              [with_bash_completion_dir=auto])\n  AS_IF([test \"x$with_bash_completion_dir\" = \"xyes\" -o \"x$with_bash_completion_dir\" = \"xauto\"], \n    [\n      PKG_CHECK_VAR(bashcompdir, [bash-completion], [completionsdir], ,\n                    bashcompdir=\"${sysconfdir}/bash_completion.d\")\n      with_bash_completion_dir=$bashcompdir\n    ])\n  AC_SUBST([BASH_COMPLETION_DIR], $with_bash_completion_dir)\n\n  AC_ARG_WITH([zsh-completion-dir],\n              [AS_HELP_STRING([--with-zsh-completion-dir=DIR], [Directory for zsh completion files])],,\n              [with_zsh_completion_dir=auto])\n  AS_IF([test \"x$with_zsh_completion_dir\" = \"xyes\" -o \"x$with_zsh_completion_dir\" = \"xauto\"], \n    [\n      with_zsh_completion_dir=\"/usr/local/share/zsh/site-functions\"\n    ])\n  AC_SUBST([ZSH_COMPLETION_DIR], $with_zsh_completion_dir)\n\n  AC_ARG_WITH([fish-completion-dir],\n\t      [AS_HELP_STRING([--with-fish-completion-dir=DIR], [Directory for fish completion files])],,\n              [with_fish_completion_dir=auto])\n  AS_IF([test \"x$with_fish_completion_dir\" = \"xyes\" -o \"x$with_fish_completion_dir\" = \"xauto\"], \n    [\n     with_fish_completion_dir=\"/usr/local/share/fish/completions\"\n    ])\n  AC_SUBST([FISH_COMPLETION_DIR], $with_fish_completion_dir)\n\n  ])\n\ndnl\ndnl Debug support\nAC_ARG_ENABLE(debug,\n  AS_HELP_STRING([--enable-debug], [Pass debug option to the compiler]))\nAS_IF([test \"x$enable_debug\" = \"xyes\"], AC_SUBST([DEBUG],yes), AC_SUBST([DEBUG],no))\n\ndnl generate necessary files\nAC_CONFIG_FILES([\n\t\t Makefile\n\t\t contrib/pacman/PKGBUILD\n\t\t contrib/spec/onedrive.spec\n\t\t onedrive.1\n\t\t contrib/systemd/onedrive.service\n\t\t contrib/systemd/onedrive@.service\n\t\t])\nAC_OUTPUT\n"
  },
  {
    "path": "contrib/completions/complete.bash",
    "content": "# BASH completion code for OneDrive Linux Client\n# (c) 2019 Norbert Preining\n# License: GPLv3+ (as with the rest of the OneDrive Linux client project)\n\n_onedrive()\n{\n\tlocal cur prev\n\n\tCOMPREPLY=()\n\tcur=${COMP_WORDS[COMP_CWORD]}\n\tprev=${COMP_WORDS[COMP_CWORD-1]}\n\n\toptions='--check-for-nomount --check-for-nosync --cleanup-local-files --debug-https --disable-notifications --display-config --display-quota --display-sync-status --disable-download-validation --disable-upload-validation --display-running-config --download-only --dry-run --enable-logging --force --force-http-11 --force-sync --list-shared-items --local-first --logout -m --monitor --no-remote-delete --print-access-token --reauth --remove-source-files --remove-source-folders --resync --resync-auth --skip-dir-strict-match --skip-dot-files --skip-symlinks -s --sync --sync-root-files --sync-shared-files --upload-only -v+ --verbose --version -h --help --with-editing-perms'\n\targopts='--auth-files --auth-response --classify-as-big-delete --confdir --create-directory --create-share-link --destination-directory --download-file --file-fragment-size --get-O365-drive-id --get-file-link --get-sharepoint-drive-id --log-dir --modified-by --monitor-fullscan-frequency --monitor-interval --monitor-log-frequency --remove-directory --share-password --single-directory --skip-dir --skip-file --skip-size --source-directory --space-reservation --syncdir --threads'\n\n\t# Loop on the arguments to manage conflicting options\n\tfor (( i=0; i < ${#COMP_WORDS[@]}-1; i++ )); do\n\t\t#exclude some mutually exclusive options\n\t\t[[ ${COMP_WORDS[i]} == '--sync' ]] && options=${options/--monitor}\n\t\t[[ ${COMP_WORDS[i]} == '--monitor' ]] && options=${options/--sync}\n\tdone\n\n\tcase \"$prev\" in\n\t--confdir|--syncdir)\n\t\t_filedir\n\t\treturn 0\n\t\t;;\n\n\t--get-file-link)\n\t\tif command -v sed &> /dev/null; then\n\t\t\tpushd \"$(onedrive --display-config | sed -n \"/sync_dir/s/.*= //p\")\" &> /dev/null\n\t\t\t_filedir\n\t\t\tpopd &> /dev/null\n\t\tfi\n\t\treturn 0\n\t\t;;\n\t--create-directory|--get-O365-drive-id|--remove-directory|--single-directory|--source-directory)\n\t\treturn 0\n\t\t;;\n\t*)\n\t\tCOMPREPLY=( $( compgen -W \"$options $argopts\" -- \"$cur\"))\n\t\treturn 0\n\t\t;;\n\tesac\n\n\t# notreached\n\treturn 0\n}\ncomplete -F _onedrive onedrive\n"
  },
  {
    "path": "contrib/completions/complete.fish",
    "content": "# FISH completions for OneDrive Linux Client\n# License: GPLv3+ (as with the rest of the OneDrive Linux client project)\n\ncomplete -c onedrive -f\n\ncomplete -c onedrive -l auth-files -d \"Authenticate using input/output files\"\ncomplete -c onedrive -l auth-response -d \"Authenticate using the response URL\"\ncomplete -c onedrive -l check-for-nomount -d \"Skip sync if .nosync found in sync dir root\"\ncomplete -c onedrive -l check-for-nosync -d \"Skip directories containing .nosync\"\ncomplete -c onedrive -l classify-as-big-delete -d \"Classify as big delete when children exceed number\"\ncomplete -c onedrive -l cleanup-local-files -d \"Cleanup local files when using --download-only\"\ncomplete -c onedrive -l confdir -d \"Directory for configuration files\"\ncomplete -c onedrive -l create-directory -d \"Create directory on OneDrive\"\ncomplete -c onedrive -l create-share-link -d \"Create a shareable link for a file\"\ncomplete -c onedrive -l debug-https -d \"Debug HTTPS communication\"\ncomplete -c onedrive -l destination-directory -d \"Target directory for move/rename operations\"\ncomplete -c onedrive -l disable-download-validation -d \"Disable validation of downloaded files\"\ncomplete -c onedrive -l disable-notifications -d \"Disable desktop notifications in monitor mode\"\ncomplete -c onedrive -l disable-upload-validation -d \"Disable validation of uploaded files\"\ncomplete -c onedrive -l display-config -d \"Display current config\"\ncomplete -c onedrive -l display-quota -d \"Display OneDrive quota\"\ncomplete -c onedrive -l display-running-config -d \"Display config used at startup\"\ncomplete -c onedrive -l display-sync-status -d \"Show current sync status\"\ncomplete -c onedrive -l download-file -d \"Download a single file from Microsoft OneDrive\"\ncomplete -c onedrive -l download-only -d \"Only download remote changes\"\ncomplete -c onedrive -l dry-run -d \"Simulate sync without making changes\"\ncomplete -c onedrive -l enable-logging -d \"Enable logging to a file\"\ncomplete -c onedrive -l file-fragment-size -d \"Specify the file fragment size for large file uploads (in MB)\"\ncomplete -c onedrive -l force -d \"Force delete on big delete detection\"\ncomplete -c onedrive -l force-http-11 -d \"Force HTTP 1.1 usage\"\ncomplete -c onedrive -l force-sync -d \"Force sync of specified folder\"\ncomplete -c onedrive -l get-file-link -d \"Get shareable link for a file\"\ncomplete -c onedrive -l get-O365-drive-id -d \"Get Drive ID for O365 SharePoint (deprecated)\"\ncomplete -c onedrive -l get-sharepoint-drive-id -d \"Get Drive ID for SharePoint\"\ncomplete -c onedrive -l help -d \"Show help message\"\ncomplete -c onedrive -l list-shared-items -d \"List shared OneDrive items\"\ncomplete -c onedrive -l local-first -d \"Prefer local changes during sync\"\ncomplete -c onedrive -l log-dir -d \"Directory for logs\"\ncomplete -c onedrive -l logout -d \"Logout current session\"\ncomplete -c onedrive -l modified-by -d \"Show who last modified a file\"\ncomplete -c onedrive -l monitor -d \"Run in monitor mode\"\ncomplete -c onedrive -l monitor-fullscan-frequency -d \"Full scan every N runs\"\ncomplete -c onedrive -l monitor-interval -d \"Sync interval in monitor mode\"\ncomplete -c onedrive -l monitor-log-frequency -d \"Log status every N seconds in monitor mode\"\ncomplete -c onedrive -l no-remote-delete -d \"Don't delete remote files in --upload-only\"\ncomplete -c onedrive -l print-access-token -d \"Show access token\"\ncomplete -c onedrive -l reauth -d \"Reauthenticate client\"\ncomplete -c onedrive -l remove-directory -d \"Delete remote directory\"\ncomplete -c onedrive -l remove-source-files -d \"Remove uploaded local files\"\ncomplete -c onedrive -l remove-source-folders -d \"Remove the local directory structure post successful file transfer\"\ncomplete -c onedrive -l resync -d \"Perform full resync\"\ncomplete -c onedrive -l resync-auth -d \"Confirm resync action\"\ncomplete -c onedrive -l share-password -d \"Password-protect shared link\"\ncomplete -c onedrive -l single-directory -d \"Sync a single local directory\"\ncomplete -c onedrive -l skip-dir -d \"Skip matching directories\"\ncomplete -c onedrive -l skip-dir-strict-match -d \"Strict matching for skipped dirs\"\ncomplete -c onedrive -l skip-dot-files -d \"Skip hidden files and folders\"\ncomplete -c onedrive -l skip-file -d \"Skip matching files\"\ncomplete -c onedrive -l skip-size -d \"Skip files above given size\"\ncomplete -c onedrive -l skip-symlinks -d \"Ignore symlinks\"\ncomplete -c onedrive -l source-directory -d \"Source path for move/rename\"\ncomplete -c onedrive -l space-reservation -d \"Reserve disk space (MB)\"\ncomplete -c onedrive -l sync -d \"Start sync operation\"\ncomplete -c onedrive -l syncdir -d \"Local sync directory\"\ncomplete -c onedrive -l synchronize -d \"Deprecated alias for --sync\"\ncomplete -c onedrive -l sync-root-files -d \"Sync root files with sync_list\"\ncomplete -c onedrive -l sync-shared-files -d \"Sync shared business files\"\ncomplete -c onedrive -l threads -d \"Specify a value for the number of worker threads used for parallel upload and download operations\"\ncomplete -c onedrive -l upload-only -d \"Only upload local changes\"\ncomplete -c onedrive -l verbose -d \"Increase verbosity\"\ncomplete -c onedrive -l version -d \"Show version\"\ncomplete -c onedrive -l with-editing-perms -d \"Create read-write shared link\"\n"
  },
  {
    "path": "contrib/completions/complete.zsh",
    "content": "#compdef onedrive\n#\n# ZSH completion code for OneDrive Linux Client\n# (c) 2019 Norbert Preining\n# License: GPLv3+ (as with the rest of the OneDrive Linux client project)\n\nlocal -a all_opts\nall_opts=(\n  '--auth-files[Perform authentication via file exchange]:auth files:'\n  '--auth-response[Perform authentication via response URL]:auth response:'\n  '--check-for-nomount[Check for the presence of .nosync in the syncdir root. If found, do not perform sync.]'\n  '--check-for-nosync[Check for the presence of .nosync in each directory. If found, skip directory from sync.]'\n  '--classify-as-big-delete[Number of children removed to trigger big delete logic]:threshold:'\n  '--cleanup-local-files[Remove local files when using --download-only]'\n  '--confdir[Set the directory used to store the configuration files]:config directory:_files -/'\n  '--create-directory[Create a directory on OneDrive - no sync will be performed.]:directory name:'\n  '--create-share-link[Create a shareable link for a file]:file name:'\n  '--debug-https[Debug OneDrive HTTPS communication.]'\n  '--destination-directory[Destination directory for renamed or move on OneDrive - no sync will be performed.]:directory name:'\n  '--disable-download-validation[Disable download validation when downloading from OneDrive]'\n  '--disable-notifications[Do not use desktop notifications in monitor mode.]'\n  '--disable-upload-validation[Disable upload validation when uploading to OneDrive]'\n  '--display-config[Display what options the client will use as currently configured - no sync will be performed.]'\n  '--display-quota[Display the quota status of the client - no sync will be performed.]'\n  '--display-running-config[Display options configured on application startup.]'\n  '--display-sync-status[Display the sync status of the client - no sync will be performed.]'\n  '--download-file[Download a single file from Microsoft OneDrive]:file name:'\n  '--download-only[Only download remote changes]'\n  '--dry-run[Perform a trial sync with no changes made]'\n  '--enable-logging[Enable client activity to a separate log file]'\n  '--file-fragment-size[Specify the file fragment size for large file uploads (in MB)]:MB:'\n  '--force[Force the deletion of data when a '\\''big delete'\\'' is detected]'\n  '--force-http-11[Force the use of HTTP 1.1 for all operations]'\n  '--force-sync[Force a synchronization of a specific folder]'\n  '--get-O365-drive-id[Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library]:site URL:'\n  '--get-file-link[Display the file link of a synced file]:file name:'\n  '--get-sharepoint-drive-id[Query and return the SharePoint Drive ID]:site URL:'\n  '--help[Show this help screen]'\n  '--list-shared-items[List OneDrive Business Shared Items]'\n  '--local-first[Synchronize from the local directory source first, before downloading changes from OneDrive.]'\n  '--log-dir[Directory where logging output is saved]:log directory:_files -/'\n  '--logout[Logout the current user]'\n  '--modified-by[Display the last modified-by details]:file or directory:'\n  '--monitor[Keep monitoring for local and remote changes]'\n  '--monitor-fullscan-frequency[Sync runs before full local scan]:N:'\n  '--monitor-interval[Seconds between syncs when idle in monitor mode]:seconds:'\n  '--monitor-log-frequency[Frequency of logging in monitor mode]:seconds:'\n  '--no-remote-delete[Do not delete remote files when using --upload-only]'\n  '--print-access-token[Print the access token, useful for debugging]'\n  '--reauth[Reauthenticate the client with OneDrive]'\n  '--remove-directory[Remove a directory on OneDrive - no sync will be performed.]:directory name:'\n  '--remove-source-files[Remove source file after upload when using --upload-only]'\n  '--remove-source-folders[Remove the local directory structure post successful file transfer when using --upload-only --remove-source-files]'\n  '--resync[Forget the last saved state, perform a full sync]'\n  '--resync-auth[Approve the use of performing a --resync action]'\n  '--share-password[Password to protect share link]:password:'\n  '--single-directory[Sync a single local directory within the OneDrive root]:source directory:_files -/'\n  '--skip-dir[Skip any directories matching this pattern]:pattern:'\n  '--skip-dir-strict-match[Strict matching for --skip-dir]'\n  '--skip-dot-files[Skip dot files and folders from syncing]'\n  '--skip-file[Skip any files matching this pattern]:pattern:'\n  '--skip-size[Skip new files larger than this size (in MB)]:MB:'\n  '--skip-symlinks[Skip syncing of symlinks]'\n  '--source-directory[Source directory to rename or move on OneDrive]:source directory:'\n  '--space-reservation[Disk space (MB) to reserve]:MB:'\n  '--sync[Perform a synchronisation with Microsoft OneDrive]'\n  '--sync-root-files[Sync all files in sync_dir root when using sync_list.]'\n  '--sync-shared-files[Sync OneDrive Business Shared Files to the local filesystem]'\n  '--syncdir[Specify the local directory used for synchronisation to OneDrive]:sync directory:_files -/'\n  '--synchronize[Perform a synchronisation (deprecated)]'\n  '--threads[Number of threads to use for multi-threaded transfers]:N:'\n  '--upload-only[Only upload to OneDrive, do not sync changes from OneDrive locally]'\n  '--verbose[Print more details, useful for debugging (repeat for extra debugging)]'\n  '--version[Print the version and exit]'\n  '--with-editing-perms[Create a read-write shareable link for a file]'\n)\n\n_arguments -S \"$all_opts[@]\" && return 0\n"
  },
  {
    "path": "contrib/docker/Dockerfile",
    "content": "# -*-Dockerfile-*-\n\nARG FEDORA_VERSION=43\nARG DEBIAN_VERSION=bullseye\nARG GO_VERSION=1.23\nARG GOSU_VERSION=1.17\n\nFROM golang:${GO_VERSION}-${DEBIAN_VERSION} AS builder-gosu\nARG GOSU_VERSION\nRUN go install -ldflags \"-s -w\" github.com/tianon/gosu@${GOSU_VERSION}\n\nFROM fedora:${FEDORA_VERSION} AS builder-onedrive\n\nRUN dnf install -y ldc pkgconf libcurl-devel sqlite-devel dbus-devel git awk\n\nENV PKG_CONFIG=/usr/bin/pkgconf\n\nCOPY . /usr/src/onedrive\nWORKDIR /usr/src/onedrive\n\nRUN ./configure --enable-debug\\\n && make clean \\\n && make \\\n && make install\n\nFROM fedora:${FEDORA_VERSION}\n\nRUN dnf clean all \\\n && dnf -y update\n\nRUN dnf install -y libcurl sqlite ldc-libs dbus-libs \\\n && dnf clean all \\\n && mkdir -p /onedrive/conf /onedrive/data\n\nCOPY --from=builder-gosu /go/bin/gosu /usr/local/bin/\nCOPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/\n\nCOPY contrib/docker/entrypoint.sh /\nRUN chmod +x /entrypoint.sh\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "contrib/docker/Dockerfile-alpine",
    "content": "# -*-Dockerfile-*-\n\nARG ALPINE_VERSION=3.23\nARG GO_VERSION=1.25\nARG GOSU_VERSION=1.17\n\nFROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder-gosu\nARG GOSU_VERSION\nRUN go install -ldflags \"-s -w\" github.com/tianon/gosu@${GOSU_VERSION}\n\nFROM alpine:${ALPINE_VERSION} AS builder-onedrive\n\nRUN apk add --update --no-cache alpine-sdk gnupg xz curl-dev sqlite-dev dbus-dev binutils-gold autoconf automake ldc\n\nCOPY . /usr/src/onedrive\nWORKDIR /usr/src/onedrive\n\nRUN autoreconf -fiv \\\n && ./configure --enable-debug\\\n && make clean \\\n && make \\\n && make install\n\nFROM alpine:${ALPINE_VERSION}\n\nRUN apk add --upgrade apk-tools \\\n && apk upgrade --available\n\nRUN apk add --update --no-cache bash libcurl libgcc shadow sqlite-libs ldc-runtime dbus-libs \\\n && mkdir -p /onedrive/conf /onedrive/data\n\nCOPY --from=builder-gosu /go/bin/gosu /usr/local/bin/\nCOPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/\n\nCOPY contrib/docker/entrypoint.sh /\nRUN chmod +x /entrypoint.sh\n\nENTRYPOINT [\"/entrypoint.sh\"]"
  },
  {
    "path": "contrib/docker/Dockerfile-debian",
    "content": "# -*-Dockerfile-*-\n\nARG DEBIAN_VERSION=trixie\n\nFROM debian:${DEBIAN_VERSION} AS builder-onedrive\nARG DEBIAN_VERSION\n\n# Add backports repository and update before initial DEBIAN_FRONTEND installation\nRUN apt-get clean \\\n && echo \"deb http://deb.debian.org/debian ${DEBIAN_VERSION}-backports main\" > /etc/apt/sources.list.d/debian-${DEBIAN_VERSION}-backports.list \\\n && apt-get update \\\n && apt-get upgrade -y \\\n && apt-get update \\\n && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential curl ca-certificates libcurl4-openssl-dev libsqlite3-dev libxml2-dev libdbus-1-dev pkg-config git ldc \\\n # Install|update curl from backports\n && apt-get install -t ${DEBIAN_VERSION}-backports -y curl \\\n && rm -rf /var/lib/apt/lists/*\n\nCOPY . /usr/src/onedrive\nWORKDIR /usr/src/onedrive\n\nRUN ./configure --enable-debug\\\n && make clean \\\n && make \\\n && make install\n\nFROM debian:${DEBIAN_VERSION}-slim\nARG DEBIAN_VERSION\n\n# Add backports repository and update after DEBIAN_FRONTEND installation\nRUN apt-get clean \\\n && echo \"deb http://deb.debian.org/debian ${DEBIAN_VERSION}-backports main\" > /etc/apt/sources.list.d/debian-${DEBIAN_VERSION}-backports.list \\\n && apt-get update \\\n && apt-get upgrade -y \\\n && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libsqlite3-0 ca-certificates libphobos2-ldc-shared110 libdbus-1-3 \\\n # Install|update curl and libcurl4t64 from backports to get the latest version\n && apt-get install -t ${DEBIAN_VERSION}-backports -y curl libcurl4t64 \\\n && rm -rf /var/lib/apt/lists/* \\\n # Fix bug with ssl on armhf: https://serverfault.com/a/1045189\n && /usr/bin/c_rehash \\\n && mkdir -p /onedrive/conf /onedrive/data\n\n# Install gosu v1.17 from trusted upstream source (built against Go 1.18.2)\nRUN set -eux; \\\n    arch=\"$(dpkg --print-architecture)\"; \\\n    curl -fsSL -o /usr/local/bin/gosu \"https://github.com/tianon/gosu/releases/download/1.17/gosu-${arch}\"; \\\n    chmod +x /usr/local/bin/gosu; \\\n    gosu nobody true\n\nCOPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/\n\nCOPY contrib/docker/entrypoint.sh /\nRUN chmod +x /entrypoint.sh\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "contrib/docker/entrypoint.sh",
    "content": "#!/bin/bash -eu\n\nset +H -euo pipefail\n\n# ----------------------------------------------------------------------\n# Determine how the container is being started:\n# - If started as non-root (e.g. --user 1000:1000), we must NOT attempt\n#   user/group management or chown, as those require root.\n# - If started as root, we can create/align the user and switch via gosu.\n# ----------------------------------------------------------------------\n\nCONTAINER_UID=\"$(id -u)\"\nCONTAINER_GID=\"$(id -g)\"\n\n# Default ONEDRIVE_UID/GID:\n# - When running as non-root: default to the current UID/GID (the values Docker/Podman set)\n# - When running as root: keep existing behaviour (infer from /onedrive/data unless explicitly provided)\nif [ \"${CONTAINER_UID}\" -ne 0 ]; then\n\t: \"${ONEDRIVE_UID:=${CONTAINER_UID}}\"\n\t: \"${ONEDRIVE_GID:=${CONTAINER_GID}}\"\nelse\n\t: \"${ONEDRIVE_UID:=$(stat /onedrive/data -c '%u')}\"\n\t: \"${ONEDRIVE_GID:=$(stat /onedrive/data -c '%g')}\"\nfi\n\n# ----------------------------------------------------------------------\n# Root privilege handling\n# ----------------------------------------------------------------------\nif [ \"${CONTAINER_UID}\" -eq 0 ]; then\n\t# Containers should not run the onedrive client as root by default.\n\tif [ \"${ONEDRIVE_RUNAS_ROOT:=0}\" == \"1\" ]; then\n\t\techo \"# Running container as root due to environment variable override\"\n\t\toduser='root'\n\t\todgroup='root'\n\telse\n\t\t# Root container start is fine, but we will drop privileges to a non-root user.\n\t\techo \"# Container started as root; will drop privileges to UID:GID ${ONEDRIVE_UID}:${ONEDRIVE_GID}\"\n\tfi\n\n\t# If we are not forcing root runtime, ensure a non-root user exists for ONEDRIVE_UID/GID\n\tif [ \"${ONEDRIVE_RUNAS_ROOT:=0}\" != \"1\" ]; then\n\t\t# Create / select group for target GID\n\t\tif ! odgroup=\"$(getent group \"${ONEDRIVE_GID}\")\"; then\n\t\t\todgroup='onedrive'\n\t\t\tgroupadd \"${odgroup}\" -g \"${ONEDRIVE_GID}\"\n\t\telse\n\t\t\todgroup=\"${odgroup%%:*}\"\n\t\tfi\n\n\t\t# Create / select user for target UID\n\t\tif ! oduser=\"$(getent passwd \"${ONEDRIVE_UID}\")\"; then\n\t\t\toduser='onedrive'\n\t\t\tuseradd -m \"${oduser}\" -u \"${ONEDRIVE_UID}\" -g \"${ONEDRIVE_GID}\"\n\t\telse\n\t\t\toduser=\"${oduser%%:*}\"\n\t\t\tusermod -g \"${odgroup}\" \"${oduser}\"\n\t\tfi\n\n\t\techo \"# Running container as user: ${oduser} (UID:GID ${ONEDRIVE_UID}:${ONEDRIVE_GID})\"\n\tfi\nelse\n\t# Non-root start (e.g. --user). Do not attempt account management or chown.\n\tif [ \"${ONEDRIVE_RUNAS_ROOT:=0}\" == \"1\" ]; then\n\t\techo \"# NOTE: ONEDRIVE_RUNAS_ROOT=1 requested, but container is not running as root; ignoring.\"\n\tfi\n\n\techo \"# Container started as non-root UID:GID ${CONTAINER_UID}:${CONTAINER_GID}\"\n\techo \"# Using ONEDRIVE_UID:GID ${ONEDRIVE_UID}:${ONEDRIVE_GID} (no user/group creation performed)\"\nfi\n\n# ----------------------------------------------------------------------\n# Default parameters\n# ----------------------------------------------------------------------\nARGS=(--confdir /onedrive/conf --syncdir /onedrive/data)\necho \"# Base Args: ${ARGS[@]}\"\n\n# Tell client to use Standalone Mode, based on an environment variable. Otherwise Monitor Mode is used.\nif [ \"${ONEDRIVE_SYNC_ONCE:=0}\" == \"1\" ]; then\n\techo \"# We run in Standalone Mode\"\n\techo \"# Adding --sync\"\n\tARGS=(--sync ${ARGS[@]})\nelse\n\techo \"# We run in Monitor Mode\"\n\techo \"# Adding --monitor\"\n\tARGS=(--monitor ${ARGS[@]})\nfi\n\n# Make Verbose output optional, based on an environment variable\nif [ \"${ONEDRIVE_VERBOSE:=0}\" == \"1\" ]; then\n\techo \"# We are being verbose\"\n\techo \"# Adding --verbose\"\n\tARGS=(--verbose ${ARGS[@]})\nfi\n\n# Tell client to perform debug output, based on an environment variable\nif [ \"${ONEDRIVE_DEBUG:=0}\" == \"1\" ]; then\n\techo \"# We are performing debug output\"\n\techo \"# Adding --verbose --verbose\"\n\tARGS=(--verbose --verbose ${ARGS[@]})\nfi\n\n# Tell client to perform HTTPS debug output, based on an environment variable\nif [ \"${ONEDRIVE_DEBUG_HTTPS:=0}\" == \"1\" ]; then\n\techo \"# We are performing HTTPS debug output\"\n\techo \"# Adding --debug-https\"\n\tARGS=(--debug-https ${ARGS[@]})\nfi\n\n# Tell client to perform a resync based on environment variable\nif [ \"${ONEDRIVE_RESYNC:=0}\" == \"1\" ]; then\n\techo \"# We are performing a --resync\"\n\techo \"# Adding --resync --resync-auth\"\n\tARGS=(--resync --resync-auth ${ARGS[@]})\nfi\n\n# Tell client to sync in download-only mode based on environment variable\nif [ \"${ONEDRIVE_DOWNLOADONLY:=0}\" == \"1\" ]; then\n\techo \"# We are synchronising in download-only mode\"\n\techo \"# Adding --download-only\"\n\tARGS=(--download-only ${ARGS[@]})\nfi\n\n# Tell client to clean up local files when in download-only mode based on environment variable\nif [ \"${ONEDRIVE_CLEANUPLOCAL:=0}\" == \"1\" ]; then\n\techo \"# We are cleaning up local files that are not present online\"\n\techo \"# Adding --cleanup-local-files\"\n\tARGS=(--cleanup-local-files ${ARGS[@]})\nfi\n\n# Tell client to sync in upload-only mode based on environment variable\nif [ \"${ONEDRIVE_UPLOADONLY:=0}\" == \"1\" ]; then\n\techo \"# We are synchronising in upload-only mode\"\n\techo \"# Adding --upload-only\"\n\tARGS=(--upload-only ${ARGS[@]})\nfi\n\n# Tell client to sync in no-remote-delete mode based on environment variable\nif [ \"${ONEDRIVE_NOREMOTEDELETE:=0}\" == \"1\" ]; then\n\techo \"# We are synchronising in no-remote-delete mode\"\n\techo \"# Adding --no-remote-delete\"\n\tARGS=(--no-remote-delete ${ARGS[@]})\nfi\n\n# Tell client to logout based on environment variable\nif [ \"${ONEDRIVE_LOGOUT:=0}\" == \"1\" ]; then\n\techo \"# We are logging out\"\n\techo \"# Adding --logout\"\n\tARGS=(--logout ${ARGS[@]})\nfi\n\n# Tell client to re-authenticate based on environment variable\nif [ \"${ONEDRIVE_REAUTH:=0}\" == \"1\" ]; then\n\techo \"# We are logging out to perform a reauthentication\"\n\techo \"# Adding --reauth\"\n\tARGS=(--reauth ${ARGS[@]})\nfi\n\n# Tell client to utilise auth files at the provided locations based on environment variable\nif [ -n \"${ONEDRIVE_AUTHFILES:=\"\"}\" ]; then\n\techo \"# We are using auth files to perform authentication\"\n\techo \"# Adding --auth-files ARG\"\n\tARGS=(--auth-files ${ONEDRIVE_AUTHFILES} ${ARGS[@]})\nfi\n\n# Tell client to utilise provided auth response based on environment variable\nif [ -n \"${ONEDRIVE_AUTHRESPONSE:=\"\"}\" ]; then\n\techo \"# We are providing the auth response directly to perform authentication\"\n\techo \"# Adding --auth-response ARG\"\n\tARGS=(--auth-response \\\"${ONEDRIVE_AUTHRESPONSE}\\\" ${ARGS[@]})\nfi\n\n# Tell client to print the running configuration at application startup\nif [ \"${ONEDRIVE_DISPLAY_CONFIG:=0}\" == \"1\" ]; then\n\techo \"# We are printing the application running configuration at application startup\"\n\techo \"# Adding --display-running-config\"\n\tARGS=(--display-running-config ${ARGS[@]})\nfi\n\n# Tell client to use sync single dir option\nif [ -n \"${ONEDRIVE_SINGLE_DIRECTORY:=\"\"}\" ]; then\n\techo \"# We are synchronising in single-directory mode\"\n\techo \"# Adding --single-directory ARG\"\n\tARGS=(--single-directory \\\"${ONEDRIVE_SINGLE_DIRECTORY}\\\" ${ARGS[@]})\nfi\n\n# Tell client run in dry-run mode\nif [ \"${ONEDRIVE_DRYRUN:=0}\" == \"1\" ]; then\n\techo \"# We are running in dry-run mode\"\n\techo \"# Adding --dry-run\"\n\tARGS=(--dry-run ${ARGS[@]})\nfi\n\n# Tell client to disable download validation\nif [ \"${ONEDRIVE_DISABLE_DOWNLOAD_VALIDATION:=0}\" == \"1\" ]; then\n\techo \"# We are disabling the download integrity checks performed by this client\"\n\techo \"# Adding --disable-download-validation\"\n\tARGS=(--disable-download-validation ${ARGS[@]})\nfi\n\n# Tell client to disable upload validation\nif [ \"${ONEDRIVE_DISABLE_UPLOAD_VALIDATION:=0}\" == \"1\" ]; then\n\techo \"# We are disabling the upload integrity checks performed by this client\"\n\techo \"# Adding --disable-upload-validation\"\n\tARGS=(--disable-upload-validation ${ARGS[@]})\nfi\n\n# Tell client to download OneDrive Business Shared Files if 'sync_business_shared_items' option has been enabled in the configuration files\nif [ \"${ONEDRIVE_SYNC_SHARED_FILES:=0}\" == \"1\" ]; then\n\techo \"# We are attempting to sync OneDrive Business Shared Files if 'sync_business_shared_items' has been enabled in the config file\"\n\techo \"# Adding --sync-shared-files\"\n\tARGS=(--sync-shared-files ${ARGS[@]})\nfi\n\n# Tell client to use a different value for file fragment size for large file uploads\nif [ -n \"${ONEDRIVE_FILE_FRAGMENT_SIZE:=\"\"}\" ]; then\n\techo \"# We are specifying the file fragment size for large file uploads (in MB)\"\n\techo \"# Adding --file-fragment-size ARG\"\n\tARGS=(--file-fragment-size ${ONEDRIVE_FILE_FRAGMENT_SIZE} ${ARGS[@]})\nfi\n\n# Tell client to use a specific threads value for parallel operations\nif [ -n \"${ONEDRIVE_THREADS:=\"\"}\" ]; then\n\techo \"# We are specifying a thread value for the number of worker threads used for parallel upload and download operations\"\n\techo \"# Adding --threads ARG\"\n\tARGS=(--threads ${ONEDRIVE_THREADS} ${ARGS[@]})\nfi\n\n# Allow override of args if command-line parameters are provided\nif [ ${#} -gt 0 ]; then\n\tARGS=(\"${@}\")\nfi\n\n# ----------------------------------------------------------------------\n# Launch\n# ----------------------------------------------------------------------\n\n# If started non-root, just run directly (no gosu, no chown).\nif [ \"${CONTAINER_UID}\" -ne 0 ]; then\n\techo \"# Launching 'onedrive' as UID:GID ${CONTAINER_UID}:${CONTAINER_GID}\"\n\texec /usr/local/bin/onedrive \"${ARGS[@]}\"\nfi\n\n# Started as root:\n# - If ONEDRIVE_RUNAS_ROOT=1: run directly as root.\n# - Otherwise: chown writable dirs and drop to oduser via gosu.\nif [ \"${ONEDRIVE_RUNAS_ROOT:=0}\" == \"1\" ]; then\n\techo \"# Launching 'onedrive' as root\"\n\texec /usr/local/bin/onedrive \"${ARGS[@]}\"\nelse\n\techo \"# Changing ownership permissions on /onedrive/data and /onedrive/conf to ${oduser}:${odgroup}\"\n\tchown \"${oduser}:${odgroup}\" /onedrive/data /onedrive/conf\n\techo \"# Launching 'onedrive' as ${oduser} via gosu\"\n\texec gosu \"${oduser}\" /usr/local/bin/onedrive \"${ARGS[@]}\"\nfi\n"
  },
  {
    "path": "contrib/docker/hooks/post_push",
    "content": "#!/bin/bash\n\nBUILD_DATE=`date \"+%Y%m%d%H%M\"`\n\ndocker tag ${IMAGE_NAME} \"${IMAGE_NAME}-${BUILD_DATE}\"\ndocker push \"${IMAGE_NAME}-${BUILD_DATE}\"\n"
  },
  {
    "path": "contrib/init.d/onedrive.init",
    "content": "#!/bin/sh\n#\n# chkconfig: 2345 20 80\n# description: Starts and stops OneDrive Client for Linux\n#\n\n# Source function library.\nif [ -f /etc/init.d/functions ] ; then\n  . /etc/init.d/functions\nelif [ -f /etc/rc.d/init.d/functions ] ; then\n  . /etc/rc.d/init.d/functions\nelse\n  exit 1\nfi\n\n# Source networking configuration.\n. /etc/sysconfig/network\n\n# Check that networking is up.\n[ ${NETWORKING} = \"no\" ] && exit 1\n\nAPP_NAME=\"OneDrive Client for Linux\"\nSTOP_TIMEOUT=${STOP_TIMEOUT-5}\nRETVAL=0\n\nstart() {\n\texport PATH=/usr/local/bin/:$PATH\n        echo -n \"Starting $APP_NAME: \"\n        daemon --user root onedrive_service.sh\n        RETVAL=$?\n        echo\n        [ $RETVAL -eq 0 ] && touch /var/lock/subsys/onedrive || \\\n           RETVAL=1\n        return $RETVAL\n}\n\nstop() {\n        echo -n \"Shutting down $APP_NAME: \"\n        killproc onedrive\n        RETVAL=$?\n        echo\n        [ $RETVAL = 0 ] && rm -f /var/lock/subsys/onedrive ${pidfile}\n}\n\nrestart() {\n        stop\n        start\n}\n\nrhstatus() {\n        status onedrive\n        return $?\n}\n\n# Allow status as non-root.\nif [ \"$1\" = status ]; then\n       rhstatus\n       exit $?\nfi\n\ncase \"$1\" in\n  start)\n        start\n        ;;\n  stop)\n        stop\n        ;;\n  restart)\n        restart\n        ;;\n  reload)\n        reload\n        ;;\n  status)\n        rhstatus\n        ;;\n  *)\n        echo \"Usage: $0 {start|stop|restart|reload|status}\"\n        exit 2\nesac\n\nexit $?\n"
  },
  {
    "path": "contrib/init.d/onedrive_service.sh",
    "content": "#!/bin/bash\n# This script is to assist in starting the onedrive client when using init.d\nAPP_OPTIONS=\"--monitor --verbose --enable-logging\"\nonedrive \"$APP_OPTIONS\" > /dev/null 2>&1 &\nexit 0\n"
  },
  {
    "path": "contrib/logrotate/onedrive.logrotate",
    "content": "# Any OneDrive Client logs configured for here \n\n/var/log/onedrive/*log {\n    # What user / group should logrotate use?\n    # Logrotate 3.8.9 or greater required otherwise:\n    #   \"unknown option 'su' -- ignoring line\" is generated \n    su root users\n    \n    # rotate log files weekly\n    weekly\n\n    # keep 4 weeks worth of backlogs\n    rotate 4\n\n    # create new (empty) log files after rotating old ones\n    create\n\n    # use date as a suffix of the rotated file\n    dateext\n   \n    # compress the log files\n    compress\n    \n    # missing files OK\n    missingok\n}\n"
  },
  {
    "path": "contrib/pacman/PKGBUILD.in",
    "content": "pkgname=onedrive\npkgver=@PACKAGE_VERSION@\npkgrel=1 # Patch-level (increment this when a patch is applied)\npkgdesc=\"OneDrive Client for Linux\"\nlicense=(\"GPL3\")\nurl=\"https://github.com/abraunegg/onedrive/\"\narch=(\"i686\" \"x86_64\")\n\ndepends=(\"curl\" \"gcc-libs\" \"glibc\" \"sqlite\")\nmakedepends=(\"dmd\" \"git\" \"tar\" \"make\")\n\nsource=(\"https://github.com/abraunegg/onedrive/archive/v$pkgver.tar.gz\")\nsha256sums=('SKIP') # Use SKIP or actual checksum\n\nprepare() {\n    cd \"$srcdir\"\n    tar -xzf \"$pkgname-$pkgver.tar.gz\" --one-top-level=\"$pkgname-$pkgver\" --strip-components 1\n}\n\nbuild() {\n    cd \"$srcdir/$pkgname-$pkgver\"\n    git init\n    git add .\n    git commit --allow-empty-message -m \"\"\n    git tag \"v$pkgver\"\n    make PREFIX=/usr onedrive\n}\n\npackage() {\n    cd \"$srcdir/$pkgname-$pkgver\"\n    make PREFIX=/usr DESTDIR=\"$pkgdir\" install\n}\n"
  },
  {
    "path": "contrib/spec/onedrive.spec.in",
    "content": "# Platform-specific default compiler selection\n%if 0%{?fedora} || 0%{?rhel} || 0%{?centos}\n%global default_dcompiler ldc\n%else\n%global default_dcompiler dmd\n%endif\n\n# Allow manual override: rpmbuild --define 'dcompiler dmd'\n%{!?dcompiler: %global dcompiler %{default_dcompiler}}\n\n# Compiler version constraints\n%global dmd_minver 2.091.1\n%global ldc_minver 1.20.1\n\n# Conditional BuildRequires\n%if \"%{dcompiler}\" == \"dmd\"\nBuildRequires: dmd >= %{dmd_minver}\n%else\n%if \"%{dcompiler}\" == \"ldc\"\nBuildRequires: ldc >= %{ldc_minver}\n%else\n%error Unsupported D compiler selected: %{dcompiler}\n%endif\n%endif\n\n# Systemd logic\n%if 0%{?fedora} || 0%{?rhel} >= 7\n%global with_systemd      1\n%else\n%global with_systemd      0\n%endif\n\n%if 0%{?rhel} >= 7\n%global rhel_unitdir      1\n%else\n%global rhel_unitdir      0\n%endif\n\nName:       onedrive\nVersion:    2.5.10\nRelease:    1%{?dist}\nSummary:    OneDrive Client for Linux\nGroup:      System Environment/Network\nLicense:    GPLv3\nURL:        https://github.com/abraunegg/onedrive\nSource0:    v%{version}.tar.gz\nBuildRoot:  %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)\n\nBuildRequires: sqlite-devel >= 3.7.15\nBuildRequires: libcurl-devel\nBuildRequires: dbus-devel\nRequires:     sqlite >= 3.7.15\nRequires:     libcurl\nRequires:     dbus\n\n%if 0%{?with_systemd}\nRequires(post):    systemd\nRequires(preun):   systemd\nRequires(postun):  systemd\n%else\nRequires(post):    chkconfig\nRequires(preun):   chkconfig\nRequires(preun):   initscripts\nRequires(postun):  initscripts\n%endif\n\n%define debug_package %{nil}\n\n%description\nFree client for Microsoft OneDrive on Linux. Supports personal, business, SharePoint, and shared folders. Built-in client-side filtering, delta sync, webhook support, and more.\n\n%prep\n%setup -q\n\n%build\n%configure --enable-debug --enable-notifications\nmake\n\n%install\n%make_install PREFIX=\"%{buildroot}\"\n\n%if 0%{?with_systemd}\n%if 0%{?rhel_unitdir}\n# RHEL/CentOS: system unit only\ninstall -D -m 0644 contrib/systemd/onedrive.service %{buildroot}%{_unitdir}/onedrive.service\ninstall -D -m 0644 contrib/systemd/onedrive@.service %{buildroot}%{_unitdir}/onedrive@.service\n%else\n# Fedora: install both system and user units\ninstall -D -m 0644 contrib/systemd/onedrive@.service %{buildroot}%{_unitdir}/onedrive@.service\ninstall -D -m 0644 contrib/systemd/onedrive.service %{buildroot}%{_userunitdir}/onedrive.service\n%endif\n%endif\n\n%clean\n\n%files\n%doc readme.md LICENSE changelog.md docs/*.md config\n%config %{_sysconfdir}/logrotate.d/onedrive\n%{_mandir}/man1/%{name}.1.gz\n%{_bindir}/%{name}\n%if 0%{?with_systemd}\n%if 0%{?rhel_unitdir}\n%{_unitdir}/%{name}.service\n%{_unitdir}/%{name}@.service\n%else\n%{_unitdir}/%{name}@.service\n%{_userunitdir}/%{name}.service\n%endif\n%else\n%{_bindir}/onedrive_service.sh\n/etc/init.d/onedrive\n%endif\n\n\n%changelog\n* Fri Jan 20 2026 - 2.5.10-1\n- Release v2.5.10 with new features, bug fixes, and enhancements\n\n* Thu Nov 06 2025 - 2.5.9-1\n- Release v2.5.9 with new features, bug fixes, and enhancements\n\n* Wed Nov 05 2025 - 2.5.8-1\n- Release v2.5.8 with new features, bug fixes, and enhancements\n\n* Tue Sep 23 2025 - 2.5.7-1\n- Release v2.5.7 with new features, bug fixes, and enhancements\n\n* Thu Jun 05 2025 - 2.5.6-1\n- Release v2.5.6 with new features, bug fixes, and enhancements\n\n* Mon Mar 17 2025 - 2.5.5-1\n- Release v2.5.5 with new features, bug fixes, and enhancements\n\n* Mon Feb 03 2025 - 2.5.4-1\n- Release v2.5.4 with new features, bug fixes, and enhancements\n\n* Sat Nov 16 2024 - 2.5.3-1\n- Release v2.5.3 with new features, bug fixes, and enhancements\n\n* Sun Sep 29 2024 - 2.5.2-1\n- Release v2.5.2 with new features, bug fixes, and enhancements\n\n* Fri Sep 27 2024 - 2.5.1-1\n- Release v2.5.1 with new features, bug fixes, and enhancements\n\n* Mon Sep 16 2024 - 2.5.0-1\n- Release v2.5.0 with new features, bug fixes, and enhancements\n\n* Wed Jun 21 2023 - 2.4.25-1\n- Release v2.4.25 with new features, bug fixes, and enhancements\n\n* Tue Jun 20 2023 - 2.4.24-1\n- Release v2.4.24 with new features, bug fixes, and enhancements\n\n* Fri Jan 06 2023 - 2.4.23-1\n- Release v2.4.23 with new features, bug fixes, and enhancements\n\n* Tue Dec 06 2022 - 2.4.22-1\n- Release v2.4.22 with new features, bug fixes, and enhancements\n\n* Tue Sep 27 2022 - 2.4.21-1\n- Release v2.4.21 with new features, bug fixes, and enhancements\n\n* Wed Jul 20 2022 - 2.4.20-1\n- Release v2.4.20 with new features, bug fixes, and enhancements\n\n* Wed Jun 15 2022 - 2.4.19-1\n- Release v2.4.19 with new features, bug fixes, and enhancements\n\n* Thu Jun 02 2022 - 2.4.18-1\n- Release v2.4.18 with new features, bug fixes, and enhancements\n\n* Sat Apr 30 2022 - 2.4.17-1\n- Release v2.4.17 with new features, bug fixes, and enhancements\n\n* Thu Mar 10 2022 - 2.4.16-1\n- Release v2.4.16 with new features, bug fixes, and enhancements\n\n* Fri Dec 31 2021 - 2.4.15-1\n- Release v2.4.15 with new features, bug fixes, and enhancements\n\n* Wed Nov 24 2021 - 2.4.14-1\n- Release v2.4.14 with new features, bug fixes, and enhancements\n\n* Sun Dec 27 2020 - 2.4.9-1\n- Release v2.4.9 with new features, bug fixes, and enhancements\n\n* Mon Nov 30 2020 - 2.4.8-1\n- Release v2.4.8 with new features, bug fixes, and enhancements\n\n* Mon Nov 09 2020 - 2.4.7-1\n- Release v2.4.7 with new features, bug fixes, and enhancements\n\n* Sun Oct 04 2020 - 2.4.6-1\n- Release v2.4.6 with new features, bug fixes, and enhancements\n\n* Thu Aug 13 2020 - 2.4.5-1\n- Release v2.4.5 with new features, bug fixes, and enhancements\n\n* Tue Aug 11 2020 - 2.4.4-1\n- Release v2.4.4 with new features, bug fixes, and enhancements\n\n* Mon Jun 29 2020 - 2.4.3-1\n- Release v2.4.3 with new features, bug fixes, and enhancements\n\n* Wed May 27 2020 - 2.4.2-1\n- Release v2.4.2 with new features, bug fixes, and enhancements\n\n* Sat May 02 2020 - 2.4.1-1\n- Release v2.4.1 with new features, bug fixes, and enhancements\n\n* Sun Mar 22 2020 - 2.4.0-1\n- Release v2.4.0 with new features, bug fixes, and enhancements\n\n* Tue Dec 31 2019 - 2.3.13-1\n- Release v2.3.13 with new features, bug fixes, and enhancements\n\n* Wed Dec 04 2019 - 2.3.12-1\n- Release v2.3.12 with new features, bug fixes, and enhancements\n\n* Tue Nov 05 2019 - 2.3.11-1\n- Release v2.3.11 with new features, bug fixes, and enhancements\n\n* Tue Oct 01 2019 - 2.3.10-1\n- Release v2.3.10 with new features, bug fixes, and enhancements\n\n* Sun Sep 01 2019 - 2.3.9-1\n- Release v2.3.9 with new features, bug fixes, and enhancements\n\n* Sun Aug 04 2019 - 2.3.8-1\n- Release v2.3.8 with new features, bug fixes, and enhancements\n\n* Wed Jul 03 2019 - 2.3.7-1\n- Release v2.3.7 with new features, bug fixes, and enhancements\n\n* Wed Jul 03 2019 - 2.3.6-1\n- Release v2.3.6 with new features, bug fixes, and enhancements\n\n* Wed Jun 19 2019 - 2.3.5-1\n- Release v2.3.5 with new features, bug fixes, and enhancements\n\n* Thu Jun 13 2019 - 2.3.4-1\n- Release v2.3.4 with new features, bug fixes, and enhancements\n\n* Tue Apr 16 2019 - 2.3.3-1\n- Release v2.3.3 with new features, bug fixes, and enhancements\n\n* Tue Apr 02 2019 - 2.3.2-1\n- Release v2.3.2 with new features, bug fixes, and enhancements\n\n* Tue Mar 26 2019 - 2.3.1-1\n- Release v2.3.1 with new features, bug fixes, and enhancements\n\n* Mon Mar 25 2019 - 2.3.0-1\n- Release v2.3.0 with new features, bug fixes, and enhancements\n\n* Tue Mar 12 2019 - 2.2.6-1\n- Release v2.2.6 with new features, bug fixes, and enhancements\n\n* Wed Jan 16 2019 - 2.2.5-1\n- Release v2.2.5 with new features, bug fixes, and enhancements\n\n* Fri Dec 28 2018 - 2.2.4-1\n- Release v2.2.4 with new features, bug fixes, and enhancements\n\n* Thu Dec 20 2018 - 2.2.3-1\n- Release v2.2.3 with new features, bug fixes, and enhancements\n\n* Thu Dec 20 2018 - 2.2.2-1\n- Release v2.2.2 with new features, bug fixes, and enhancements\n\n* Tue Dec 04 2018 - 2.2.1-1\n- Release v2.2.1 with new features, bug fixes, and enhancements\n\n* Sat Nov 24 2018 - 2.2.0-1\n- Release v2.2.0 with new features, bug fixes, and enhancements\n\n* Thu Nov 15 2018 - 2.1.6-1\n- Release v2.1.6 with new features, bug fixes, and enhancements\n\n* Sun Nov 11 2018 - 2.1.5-1\n- Release v2.1.5 with new features, bug fixes, and enhancements\n\n* Wed Oct 10 2018 - 2.1.4-1\n- Release v2.1.4 with new features, bug fixes, and enhancements\n\n* Thu Oct 04 2018 - 2.1.3-1\n- Release v2.1.3 with new features, bug fixes, and enhancements\n\n* Mon Aug 27 2018 - 2.1.2-1\n- Release v2.1.2 with new features, bug fixes, and enhancements\n\n* Tue Aug 14 2018 - 2.1.1-1\n- Release v2.1.1 with new features, bug fixes, and enhancements\n\n* Fri Aug 10 2018 - 2.1.0-1\n- Release v2.1.0 with new features, bug fixes, and enhancements\n\n* Wed Jul 18 2018 - 2.0.2-1\n- Release v2.0.2 with new features, bug fixes, and enhancements\n\n* Wed Jul 11 2018 - 2.0.1-1\n- Release v2.0.1 with new features, bug fixes, and enhancements\n\n* Tue Jul 10 2018 - 2.0.0-1\n- Release v2.0.0 with new features, bug fixes, and enhancements\n\n* Thu May 17 2018 - 1.1.2-1\n- Release v1.1.2 with new features, bug fixes, and enhancements\n\n* Sat Jan 20 2018 - 1.1.1-1\n- Release v1.1.1 with new features, bug fixes, and enhancements\n\n* Fri Jan 19 2018 - 1.1.0-1\n- Release v1.1.0 with new features, bug fixes, and enhancements\n\n* Tue Aug 01 2017 - 1.0.1-1\n- Release v1.0.1 with new features, bug fixes, and enhancements\n\n* Fri Jul 14 2017 - 1.0.0-1\n- Release v1.0.0 with new features, bug fixes, and enhancements"
  },
  {
    "path": "contrib/systemd/onedrive.service.in",
    "content": "[Unit]\nDescription=OneDrive Client for Linux\nDocumentation=https://github.com/abraunegg/onedrive\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\n# Commented out hardenings are disabled because they may not work out of the box on your distribution\n# If you know what you are doing please try to enable them.\n\nProtectSystem=full\n#PrivateUsers=true\n#PrivateDevices=true\nProtectHostname=true\n#ProtectClock=true\nProtectKernelTunables=true\n#ProtectKernelModules=true\n#ProtectKernelLogs=true\nProtectControlGroups=true\nRestrictRealtime=true\nExecStartPre=/bin/sh -c 'sleep 15'\nExecStart=@prefix@/bin/onedrive --monitor\nRestart=on-failure\nRestartSec=3\n# Do not restart the service if a --resync is required which is done via a 126 exit code\nRestartPreventExitStatus=126\n# Time to wait for the service to stop gracefully before forcefully terminating it\nTimeoutStopSec=90\n\n[Install]\nWantedBy=default.target"
  },
  {
    "path": "contrib/systemd/onedrive@.service.in",
    "content": "[Unit]\nDescription=OneDrive Client for Linux running for %i\nDocumentation=https://github.com/abraunegg/onedrive\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\n# Commented out hardenings are disabled because they may not work out of the box on your distribution\n# If you know what you are doing please try to enable them.\n\nProtectSystem=full\n\n#PrivateDevices=true\nProtectHostname=true\n#ProtectClock=true\nProtectKernelTunables=true\n#ProtectKernelModules=true\n#ProtectKernelLogs=true\nProtectControlGroups=true\nRestrictRealtime=true\nExecStartPre=/bin/sh -c 'sleep 15'\nExecStart=@prefix@/bin/onedrive --monitor --confdir=/home/%i/.config/onedrive\nUser=%i\nGroup=users\nRestart=on-failure\nRestartSec=3\n# Do not restart the service if a --resync is required which is done via a 126 exit code\nRestartPreventExitStatus=126\n# Time to wait for the service to stop gracefully before forcefully terminating it\nTimeoutStopSec=90\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "docs/advanced-usage.md",
    "content": "# Advanced Configuration of the OneDrive Client for Linux\nThis document covers the following scenarios:\n*   [Configuring the client to use multiple OneDrive accounts / configurations](#configuring-the-client-to-use-multiple-onedrive-accounts--configurations)\n*   [Configuring the client to use multiple OneDrive accounts / configurations using Docker](#configuring-the-client-to-use-multiple-onedrive-accounts--configurations-using-docker)\n*   [Configuring the client for use in dual-boot (Windows / Linux) situations](#configuring-the-client-for-use-in-dual-boot-windows--linux-situations)\n*   [Configuring the client for use when 'sync_dir' is a mounted directory](#configuring-the-client-for-use-when-sync_dir-is-a-mounted-directory)\n*   [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)\n\n## Configuring the client to use multiple OneDrive accounts / configurations\nEssentially, 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:\n1.  Create a unique configuration folder for each onedrive client configuration that you need\n2.  Copy to this folder a copy of the default configuration file\n3.  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\n4.  Authenticate the client using the new configuration directory\n5.  Test the configuration using '--display-config' and '--dry-run'\n6.  Sync the OneDrive account data as required using `--synchronize` or `--monitor`\n7.  Configure a unique systemd service file for this account configuration\n\n### 1. Create a unique configuration folder for each onedrive client configuration that you need\nMake the configuration folder as required for this new configuration, for example:\n```text\nmkdir ~/.config/my-new-config\n```\n\n### 2. Copy to this folder a copy of the default configuration file\nCopy 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:\n```text\nwget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/my-new-config/config\n```\n\n### 3. Update the default configuration file\nThe following config options *must* be updated to ensure that individual account data is not cross populated with other OneDrive accounts or other configurations:\n* sync_dir\n\nOther options that may require to be updated, depending on the OneDrive account that is being configured:\n*   drive_id\n*   application_id\n*   sync_business_shared_folders\n*   skip_dir\n*   skip_file\n*   Creation of a 'sync_list' file if required\n*   Creation of a 'business_shared_folders' file if required\n\n### 4. Authenticate the client\nAuthenticate the client using the specific configuration file:\n```text\nonedrive --confdir=\"~/.config/my-new-config\"\n```\nYou 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.\n```text\n[user@hostname ~]$ onedrive --confdir=\"~/.config/my-new-config\"\nConfiguration file successfully loaded\nConfiguring Global Azure AD Endpoints\nAuthorize this app visiting:\n\nhttps://.....\n\nEnter the response uri: \n\n```\n\n### 5. Display and Test the configuration\nTest 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.\n\n#### Display the configuration\n```text\nonedrive --confdir=\"~/.config/my-new-config\" --display-config\n```\n\n#### Test the configuration by performing a dry-run\n```text\nonedrive --confdir=\"~/.config/my-new-config\" --synchronize --verbose --dry-run\n```\n\nIf 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.\n\n### 6. Sync the OneDrive account data as required\nSync the data for the new account configuration as required:\n```text\nonedrive --confdir=\"~/.config/my-new-config\" --synchronize --verbose\n```\nor \n```text\nonedrive --confdir=\"~/.config/my-new-config\" --monitor --verbose\n```\n\n*   `--synchronize` does a one-time sync\n*   `--monitor` keeps the application running and monitoring for changes both local and remote\n\n### 7. Automatic syncing of new OneDrive configuration\nIn 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:\n*   RHEL / CentOS: `/usr/lib/systemd/system`\n*   Others: `/usr/lib/systemd/user` and `/lib/systemd/system`\n\n### Step1: Create a new systemd service file\n#### Red Hat Enterprise Linux, CentOS Linux\nCopy the required service file to a new name:\n```text\nsudo cp /usr/lib/systemd/system/onedrive.service /usr/lib/systemd/system/onedrive-my-new-config\n```\nor \n```text\nsudo cp /usr/lib/systemd/system/onedrive@.service /usr/lib/systemd/system/onedrive-my-new-config@.service\n```\n\n#### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora\nCopy the required service file to a new name:\n```text\nsudo cp /usr/lib/systemd/user/onedrive.service /usr/lib/systemd/user/onedrive-my-new-config.service\n```\nor \n```text\nsudo cp /lib/systemd/system/onedrive@.service /lib/systemd/system/onedrive-my-new-config@.service\n```\n\n### Step 2: Edit new systemd service file\nEdit the new systemd file, updating the line beginning with `ExecStart` so that the confdir mirrors the one you used above:\n```text\nExecStart=/usr/local/bin/onedrive --monitor --confdir=\"/full/path/to/config/dir\"\n```\n\nExample:\n```text\nExecStart=/usr/local/bin/onedrive --monitor --confdir=\"/home/myusername/.config/my-new-config\"\n```\n\n> [!IMPORTANT]\n> 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.\n\n\n### Step 3: Enable the new systemd service\nOnce the file is correctly edited, you can enable the new systemd service using the following commands.\n\n#### Red Hat Enterprise Linux, CentOS Linux\n```text\nsystemctl enable onedrive-my-new-config\nsystemctl start onedrive-my-new-config\n```\n\n#### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora\n```text\nsystemctl --user enable onedrive-my-new-config\nsystemctl --user start onedrive-my-new-config\n```\nor\n```text\nsystemctl --user enable onedrive-my-new-config@myusername.service\nsystemctl --user start onedrive-my-new-config@myusername.service\n```\n\n### Step 4: Viewing systemd status and logs for the custom service\n#### Viewing systemd service status - Red Hat Enterprise Linux, CentOS Linux\n```text\nsystemctl status onedrive-my-new-config\n```\n\n#### Viewing systemd service status - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora\n```text\nsystemctl --user status onedrive-my-new-config\n```\n\n#### Viewing journalctl systemd logs - Red Hat Enterprise Linux, CentOS Linux\n```text\njournalctl --unit=onedrive-my-new-config -f\n```\n\n#### Viewing journalctl systemd logs - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora\n```text\njournalctl --user --unit=onedrive-my-new-config -f\n```\n\n### Step 5: (Optional) Run custom systemd service at boot without user login\nIn some cases it may be desirable for the systemd service to start without having to login as your 'user'\n\nAll 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.\n\nTo 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:\n```text\nloginctl enable-linger <your_user_name>\n```\n\nExample:\n```text\nalex@ubuntu-headless:~$ loginctl enable-linger alex\n```\n\nRepeat these steps for each OneDrive new account that you wish to use.\n\n## Configuring the client to use multiple OneDrive accounts / configurations using Docker\nIn some situations it may be desirable to run multiple Docker containers at the same time, each with their own configuration.\n\nTo run the Docker container successfully, it needs two unique Docker volumes to operate:\n*   Your configuration Docker volumes\n*   Your data Docker volume\n\nWhen running multiple Docker containers, this is no different - each Docker container must have it's own configuration and data volume.\n\n### High level steps:\n1.   Create the required unique Docker volumes for the configuration volume\n2.   Create the required unique local path used for the Docker data volume\n3.   Start the multiple Docker containers with the required configuration for each container\n\n#### Create the required unique Docker volumes for the configuration volume\nCreate the required unique Docker volumes for the configuration volume(s):\n```text\ndocker volume create onedrive_conf_sharepoint_site1\ndocker volume create onedrive_conf_sharepoint_site2\ndocker volume create onedrive_conf_sharepoint_site3\n...\ndocker volume create onedrive_conf_sharepoint_site50\n```\n\n#### Create the required unique local path used for the Docker data volume\nCreate the required unique local path used for the Docker data volume\n```text\nmkdir -p /use/full/local/path/no/tilde/SharePointSite1\nmkdir -p /use/full/local/path/no/tilde/SharePointSite2\nmkdir -p /use/full/local/path/no/tilde/SharePointSite3\n...\nmkdir -p /use/full/local/path/no/tilde/SharePointSite50\n```\n\n#### Start the Docker container with the required configuration (example)\n```text\ndocker run -it --name onedrive -v onedrive_conf_sharepoint_site1:/onedrive/conf -v \"/use/full/local/path/no/tilde/SharePointSite1:/onedrive/data\" driveone/onedrive:latest\ndocker run -it --name onedrive -v onedrive_conf_sharepoint_site2:/onedrive/conf -v \"/use/full/local/path/no/tilde/SharePointSite2:/onedrive/data\" driveone/onedrive:latest\ndocker run -it --name onedrive -v onedrive_conf_sharepoint_site3:/onedrive/conf -v \"/use/full/local/path/no/tilde/SharePointSite3:/onedrive/data\" driveone/onedrive:latest\n...\ndocker run -it --name onedrive -v onedrive_conf_sharepoint_site50:/onedrive/conf -v \"/use/full/local/path/no/tilde/SharePointSite50:/onedrive/data\" driveone/onedrive:latest\n```\n\n> [!TIP]\n> 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.\n>\n> If the account credentials are different .. you will need to re-authenticate each Docker container individually.\n\n## Configuring the client for use in dual-boot (Windows / Linux) situations\nWhen 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.\n\nWhen 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.\n\nTo 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`.\n\nTo 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\".\n\nAfter 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.\n\n| OneDrive Personal | Onedrive Business<br>SharePoint |\n|---|---|\n| ![Uncheck-Personal](./images/personal-files-on-demand.png) | ![Uncheck-Business](./images/business-files-on-demand.png) |\n\n### Accessing Windows OneDrive Files from Linux (Dual-Boot Setup)\nWhen dual-booting between Windows and Linux, accessing OneDrive-synced folders stored on an NTFS partition can be problematic. This is primarily due to Microsoft OneDrive's use of reparse points when the Files On-Demand feature is enabled in Windows. These reparse points can render files inaccessible from Linux, even after disabling Files On-Demand, because the reparse metadata may persist.\n\n#### Solution: Use the ntfs-3g-onedrive Plugin\nThe ['ntfs-3g-onedrive'](https://github.com/gbrielgustavo/ntfs-3g-onedrive) plugin is designed to address this issue. It modifies the behavior of the ntfs-3g driver to correctly handle OneDrive's reparse points, allowing you to access your OneDrive files from Linux.\n\n> [!IMPORTANT]\n> The configuration and installation of the 'ntfs-3g-onedrive' driver update on your platform is beyond the scope of this documentation and repository.\n>\n> For assistance please seek support via the ['ntfs-3g'](https://github.com/tuxera/ntfs-3g) GitHub project.\n\n## Configuring the client for use when 'sync_dir' is a mounted directory\nIn 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:\n```text\nsync_dir = \"/path/to/mountpoint/OneDrive\" \n```\n\nThe issue here is - how does the client react if the mount point gets removed - network loss, device removal?\n\nThe 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.\n\nThere are a few options here which you can configure in your 'config' file to assist you to prevent this sort of item from occurring:\n1. classify_as_big_delete\n2. check_nomount\n3. check_nosync\n\n> [!NOTE] \n> Before making any change to your configuration, stop any sync process & stop any onedrive systemd service from running.\n\n### classify_as_big_delete\nBy 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\n\n### check_nomount & check_nosync\n\nWhen configuring the OneDrive client to use a directory on a mounted volume (e.g., external disk, USB device, network share), it is essential to guard against accidental sync deletion if the mount point becomes unavailable.\n\nIf a mount is lost or not yet available at the time of sync, the 'sync_dir' may appear empty, leading the client to delete the corresponding online content. To safely prevent this, enable the following configuration options:\n```\ncheck_nomount = \"true\"\ncheck_nosync  = \"true\"\n```\nThese settings instruct the client to:\n* Check for the presence of a `.nosync` file in the 'sync_dir' before syncing\n* Halt syncing immediately if the file is detected, assuming the mount has failed or not available\n\n#### How the `.nosync` file works\n1. The `.nosync` file is placed on the local filesystem, in the exact directory that will later be covered by the mounted volume.\n2. Once the external device is mounted, that directory (and the `.nosync` file) becomes hidden by the mount.\n3. If the mount disappears or fails, the `.nosync` file becomes visible again.\n4. The OneDrive client detects this and stops syncing, preventing accidental deletions due to the mount being unavailable.\n\n#### Scenario 1: 'sync_dir' points directly to a mounted path\n```\nsync_dir = \"/mnt/external/path/to/users/data/location/OneDrive\"\ncheck_nomount = \"true\"\ncheck_nosync  = \"true\"\n```\n\n**Step 1:** Before mounting the device, prepare the `.nosync` file\n```\nsudo mkdir -p /mnt/external/path/to/users/data/location/OneDrive\nsudo touch /mnt/external/path/to/users/data/location/OneDrive/.nosync\n```\n\n**Step 2:** Test the 'onedrive' Client\n```\nonedrive -s\n```\nwith the output\n```\n...\nConfiguring Global Azure AD Endpoints\nERROR: .nosync file found in directory mount point. Aborting application startup process to safeguard data.\nAttempting to perform a database vacuum to optimise database\n...\n```\n\n**Step 3:** Mount your device (e.g., via systemd, fstab, or manually)\n```\nsudo mount /dev/sdX1 /mnt/external\n```\n\n**Result:**\nThe OneDrive client will now treat `/mnt/external/path/to/users/data/location/OneDrive` as the sync_dir. If the mount is ever lost, the `.nosync` file becomes visible again, and syncing is halted. \n\n#### Scenario 2: 'sync_dir' is a symbolic link to a mounted directory\n```\nsync_dir = \"~/OneDrive\"\ncheck_nomount = \"true\"\ncheck_nosync  = \"true\"\n```\nand\n```\n$ ls -l ~/OneDrive\nlrwxrwxrwx 1 user user 29 Jul 25 14:44 OneDrive -> /mnt/external/path/to/users/data/location/OneDrive\n```\n\n**Step 1:** Before mounting the device, prepare the `.nosync` file\n```\nsudo mkdir -p /mnt/external/path/to/users/data/location/OneDrive\nsudo touch /mnt/external/path/to/users/data/location/OneDrive/.nosync\n```\n\n**Step 2:** Test the 'onedrive' Client\n```\nonedrive -s\n```\nwith the output\n```\n...\nConfiguring Global Azure AD Endpoints\nERROR: .nosync file found in directory mount point. Aborting application startup process to safeguard data.\nAttempting to perform a database vacuum to optimise database\n...\n```\n\n**Step 3:** Mount your device (e.g., via systemd, fstab, or manually)\n```\nsudo mount /dev/sdX1 /mnt/external\n```\n\n**Result:**\nYour symlinked `~/OneDrive` path will now point into the mounted filesystem. If the mount goes missing, the `.nosync` file reappears via the symlink, and the client halts syncing automatically.\n\n\n## Upload data from the local ~/OneDrive folder to a specific location on OneDrive\nIn some environments, you may not want your local ~/OneDrive folder to be uploaded directly to the root of your OneDrive account online.\n\nUnfortunately, the OneDrive API lacks any facility to perform a re-direction of data during upload.\n\nThe workaround for this is to structure your local filesystem and reconfigure your client to achieve the desired goal.\n\n### High level steps:\n1.   Create a new folder, for example `/opt/OneDrive`\n2.   Configure your application config 'sync_dir' to look at this folder\n3.   Inside `/opt/OneDrive` create the folder you wish to sync the data online to, for example: `/opt/OneDrive/RemoteOnlineDestination`\n4.   Configure the application to only sync `/opt/OneDrive/RemoteDestination` via 'sync_list'\n5.   Symbolically link `~/OneDrive` -> `/opt/OneDrive/RemoteOnlineDestination`\n\n### Outcome:\n*   Your `~/OneDrive` will look / feel as per normal\n*   The data will be stored online under `/RemoteOnlineDestination`\n\n### Testing:\n*   Validate your configuration with `onedrive --display-config`\n*   Test your configuration with `onedrive --dry-run`\n"
  },
  {
    "path": "docs/application-config-options.md",
    "content": "# Application Configuration Options for the OneDrive Client for Linux\n## Application Version\nBefore 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.\n\n## Table of Contents\n\n- [Configuration File Options](#configuration-file-options)\n  - [application_id](#application_id)\n  - [azure_ad_endpoint](#azure_ad_endpoint)\n  - [azure_tenant_id](#azure_tenant_id)\n  - [bypass_data_preservation](#bypass_data_preservation)\n  - [check_nomount](#check_nomount)\n  - [check_nosync](#check_nosync)\n  - [classify_as_big_delete](#classify_as_big_delete)\n  - [cleanup_local_files](#cleanup_local_files)\n  - [connect_timeout](#connect_timeout)\n  - [create_new_file_version](#create_new_file_version)\n  - [data_timeout](#data_timeout)\n  - [debug_https](#debug_https)\n  - [delay_inotify_processing](#delay_inotify_processing)\n  - [disable_download_validation](#disable_download_validation)\n  - [disable_notifications](#disable_notifications)\n  - [disable_permission_set](#disable_permission_set)\n  - [disable_upload_validation](#disable_upload_validation)\n  - [disable_version_check](#disable_version_check)\n  - [disable_websocket_support](#disable_websocket_support)\n  - [display_manager_integration](#display_manager_integration)\n  - [display_running_config](#display_running_config)\n  - [display_transfer_metrics](#display_transfer_metrics)\n  - [dns_timeout](#dns_timeout)\n  - [download_only](#download_only)\n  - [drive_id](#drive_id)\n  - [dry_run](#dry_run)\n  - [enable_logging](#enable_logging)\n  - [file_fragment_size](#file_fragment_size)\n  - [force_http_11](#force_http_11)\n  - [force_session_upload](#force_session_upload)\n  - [inotify_delay](#inotify_delay)\n  - [ip_protocol_version](#ip_protocol_version)\n  - [local_first](#local_first)\n  - [log_dir](#log_dir)\n  - [max_curl_idle](#max_curl_idle)\n  - [monitor_fullscan_frequency](#monitor_fullscan_frequency)\n  - [monitor_interval](#monitor_interval)\n  - [monitor_log_frequency](#monitor_log_frequency)\n  - [no_remote_delete](#no_remote_delete)\n  - [notify_file_actions](#notify_file_actions)\n  - [operation_timeout](#operation_timeout)\n  - [permanent_delete](#permanent_delete)\n  - [rate_limit](#rate_limit)\n  - [read_only_auth_scope](#read_only_auth_scope)\n  - [recycle_bin_path](#recycle_bin_path)\n  - [remove_source_files](#remove_source_files)\n  - [resync](#resync)\n  - [resync_auth](#resync_auth)\n  - [skip_dir](#skip_dir)\n  - [skip_dir_strict_match](#skip_dir_strict_match)\n  - [skip_dotfiles](#skip_dotfiles)\n  - [skip_file](#skip_file)\n  - [skip_size](#skip_size)\n  - [skip_symlinks](#skip_symlinks)\n  - [space_reservation](#space_reservation)\n  - [sync_business_shared_items](#sync_business_shared_items)\n  - [sync_dir](#sync_dir)\n  - [sync_dir_permissions](#sync_dir_permissions)\n  - [sync_file_permissions](#sync_file_permissions)\n  - [sync_root_files](#sync_root_files)\n  - [threads](#threads)\n  - [transfer_order](#transfer_order)\n  - [upload_only](#upload_only)\n  - [use_device_auth](#use_device_auth)\n  - [use_intune_sso](#use_intune_sso)\n  - [use_recycle_bin](#use_recycle_bin)\n  - [user_agent](#user_agent)\n  - [webhook_enabled](#webhook_enabled)\n  - [webhook_expiration_interval](#webhook_expiration_interval)\n  - [webhook_listening_host](#webhook_listening_host)\n  - [webhook_listening_port](#webhook_listening_port)\n  - [webhook_public_url](#webhook_public_url)\n  - [webhook_renewal_interval](#webhook_renewal_interval)\n  - [write_xattr_data](#write_xattr_data)\n- [Command Line Interface (CLI) Only Options](#command-line-interface-cli-only-options)\n  - [CLI Option: --auth-files](#cli-option---auth-files)\n  - [CLI Option: --auth-response](#cli-option---auth-response)\n  - [CLI Option: --confdir](#cli-option---confdir)\n  - [CLI Option: --create-directory](#cli-option---create-directory)\n  - [CLI Option: --create-share-link](#cli-option---create-share-link)\n  - [CLI Option: --destination-directory](#cli-option---destination-directory)\n  - [CLI Option: --display-config](#cli-option---display-config)\n  - [CLI Option: --display-sync-status](#cli-option---display-sync-status)\n  - [CLI Option: --display-quota](#cli-option---display-quota)\n  - [CLI Option: --download-file](#cli-option---download-file)\n  - [CLI Option: --force](#cli-option---force)\n  - [CLI Option: --force-sync](#cli-option---force-sync)\n  - [CLI Option: --get-file-link](#cli-option---get-file-link)\n  - [CLI Option: --get-sharepoint-drive-id](#cli-option---get-sharepoint-drive-id)\n  - [CLI Option: --list-shared-items](#cli-option---list-shared-items)\n  - [CLI Option: --logout](#cli-option---logout)\n  - [CLI Option: --modified-by](#cli-option---modified-by)\n  - [CLI Option: --monitor | -m](#cli-option---monitor--m)\n  - [CLI Option: --print-access-token](#cli-option---print-access-token)\n  - [CLI Option: --reauth](#cli-option---reauth)\n  - [CLI Option: --remove-directory](#cli-option---remove-directory)\n  - [CLI Option: --share-password](#cli-option---share-password)\n  - [CLI Option: --single-directory](#cli-option---single-directory)\n  - [CLI Option: --source-directory](#cli-option---source-directory)\n  - [CLI Option: --sync | -s](#cli-option---sync--s)\n  - [CLI Option: --sync-shared-files](#cli-option---sync-shared-files)\n  - [CLI Option: --verbose | -v+](#cli-option---verbose--v)\n  - [CLI Option: --with-editing-perms](#cli-option---with-editing-perms)\n- [Deprecated Configuration File and CLI Options](#deprecated-configuration-file-and-cli-options)\n  - [force_http_2](#force_http_2)\n  - [min_notify_changes](#min_notify_changes)\n  - [CLI Option: --synchronize](#cli-option---synchronize)\n\n\n## Configuration File Options\n\n### application_id\n_**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\thttps://portal.azure.com/, then use your new application id with this config option. You can find instructions for configuring your own app registration in [national-cloud-deployments.md](national-cloud-deployments.md) even if you don't necessarily configure it for a national cloud environment.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ d50ca740-c83f-4d1b-b616-12c519384f0c\n\n_**Config Example:**_ `application_id = \"d50ca740-c83f-4d1b-b616-12c519384f0c\"`\n\n### azure_ad_endpoint\n_**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.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ *Empty* - not required for normal operation\n\n_**Valid Values:**_ USL4, USL5, DE, CN\n\n_**Config Example:**_ `azure_ad_endpoint = \"DE\"`\n\n### azure_tenant_id\n_**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.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ *Empty* - not required for normal operation\n\n_**Config Example:**_ `azure_tenant_id = \"example.onmicrosoft.us\"` or `azure_tenant_id = \"0c4be462-a1ab-499b-99e0-da08ce52a2cc\"`\n\n> [!IMPORTANT]\n> Must be configured if 'azure_ad_endpoint' is configured.\n\n### bypass_data_preservation\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `bypass_data_preservation = \"false\"` or `bypass_data_preservation = \"true\"`\n\n### check_nomount\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `check_nomount = \"false\"` or `check_nomount = \"true\"`\n\n_**CLI Option:**_ `--check-for-nomount`\n\n> [!TIP]\n> 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.\n\n### check_nosync\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `check_nosync = \"false\"` or `check_nosync = \"true\"`\n\n_**CLI Option Use:**_ `--check-for-nosync`\n\n> [!IMPORTANT]\n> Create a `.nosync` file in any *local* directory that you wish to not sync to Microsoft OneDrive when you enable this option.\n\n### classify_as_big_delete\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 1000\n\n_**Config Example:**_ `classify_as_big_delete = \"2000\"`\n\n_**CLI Option Use:**_ `--classify-as-big-delete 2000`\n\n> [!NOTE]\n> If this option is triggered, you will need to add `--force` to force a sync to occur.\n\n### cleanup_local_files\n_**Description:**_ This config option provides the capability to cleanup local files and folders if they are removed online.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `cleanup_local_files = \"false\"` or `cleanup_local_files = \"true\"`\n\n_**CLI Option Use:**_ `--cleanup-local-files`\n\n> [!IMPORTANT]\n> This configuration option can only be used with `--download-only`. It cannot be used with any other application option.\n\n### connect_timeout\n_**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).\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 10\n\n_**Config Example:**_ `connect_timeout = \"15\"`\n\n### create_new_file_version\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `create_new_file_version = \"false\"` or `create_new_file_version = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n> [!IMPORTANT]\n> 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.\n\n> [!WARNING]\n> When this option is set to 'true', new file versions will be created online which will count towards your Microsoft OneDrive Quota.\n\n### data_timeout\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 60\n\n_**Config Example:**_ `data_timeout = \"300\"`\n\n### debug_https\n_**Description:**_ This setting controls whether the curl library is configured to output additional data to assist with diagnosing HTTPS issues and problems.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `debug_https = \"false\"` or `debug_https = \"true\"`\n\n_**CLI Option Use:**_ `--debug-https`\n\n> [!WARNING]\n> 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.\n\n\n### delay_inotify_processing\n_**Description:**_ This setting controls whether 'inotify' events should be delayed or not. This option should only ever be enabled when attempting to reduce the impact of editors like Obsidian which constantly write change to disk in an atomic fashion.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `delay_inotify_processing = \"false\"` or `delay_inotify_processing = \"true\"`\n\n> [!NOTE]\n> If you enable this option you *must* also enable 'force_session_upload' to ensure that your data uploads are done in a manner that editors, like Obsidian expect.\n\n\n### disable_download_validation\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `disable_download_validation = \"false\"` or `disable_download_validation = \"true\"`\n\n_**CLI Option Use:**_ `--disable-download-validation`\n\n> [!CAUTION]\n> 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.\n\n> [!CAUTION]\n> 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. \n>\n> By default these files will fail integrity checking and be deleted, meaning that AIP files will not reside on your platform. \n> \n> 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.\n\n### disable_notifications\n_**Description:**_ This setting controls whether GUI notifications are sent from the client to your display manager session. \n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `disable_notifications = \"false\"` or `disable_notifications = \"true\"`\n\n_**CLI Option Use:**_ `--disable-notifications`\n\n### disable_permission_set\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `disable_permission_set = \"false\"` or `disable_permission_set = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n### disable_upload_validation\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `disable_upload_validation = \"false\"` or `disable_upload_validation = \"true\"`\n\n_**CLI Option Use:**_ `--disable-upload-validation`\n\n> [!CAUTION]\n> 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.\n\n### disable_version_check\n_**Description:**_ This option determines whether the client will check the GitHub API for the current application version and grace period of running older application versions\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `disable_version_check = \"false\"` or `disable_version_check = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n### disable_websocket_support\n_**Description:**_ This option disables the built-in WebSocket support that leverages RFC6455 to communicate with the Microsoft Graph API Service, providing near real-time notifications of online changes.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `disable_websocket_support = \"false\"` or `disable_websocket_support = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n\n### display_manager_integration\n_**Description:**_ Controls whether the client integrates the configured 'sync_dir' with the desktop’s file manager (e.g. Nautilus for GNOME, Dolphin for KDE), adding it as a “special place” in the sidebar and setting a custom OneDrive folder icon where supported.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `display_manager_integration = \"false\"` or `display_manager_integration = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n### display_running_config\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `display_running_config = \"false\"` or `display_running_config = \"true\"`\n\n_**CLI Option Use:**_ `--display-running-config`\n\n### display_transfer_metrics\n_**Description:**_ This option will display file transfer metrics when enabled.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `display_transfer_metrics = \"false\"` or `display_transfer_metrics = \"true\"`\n\n_**Output Example:**_ `Transfer Metrics -  File: path/to/file.data | Size: 35768 Bytes | Duration: 2.27 Seconds | Speed: 0.02 Mbps (approx)`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n### dns_timeout\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 60\n\n_**Config Example:**_ `dns_timeout = \"90\"`\n\n### download_only\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `download_only = \"false\"` or `download_only = \"true\"`\n\n_**CLI Option Use:**_ `--download-only`\n\n> [!IMPORTANT]\n> 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`.\n\n### drive_id\n_**Description:**_ This setting controls the specific drive identifier the client will use when syncing with Microsoft OneDrive.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ *None*\n\n_**Config Example:**_ `drive_id = \"b!bO8V6s9SSk9R7mWhpIjUrotN73WlW3tEv3OxP_QfIdQimEdOHR-1So6CqeG1MfDB\"`\n\n> [!NOTE]\n> 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.\n\n### dry_run\n_**Description:**_ This setting controls the application capability to test your application configuration without actually performing any actual activity (download, upload, move, delete, folder creation).\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `dry_run = \"false\"` or `dry_run = \"true\"`\n\n_**CLI Option Use:**_ `--dry-run`\n\n### enable_logging\n_**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\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `enable_logging = \"false\"` or `enable_logging = \"true\"`\n\n_**CLI Option Use:**_ `--enable-logging`\n\n> [!IMPORTANT]\n> 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\n\n### file_fragment_size\n_**Description:**_ This option controls the fragment size when uploading large files to Microsoft OneDrive. The value specified is in MB.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 10\n\n_**Minimum Value:**_ 10\n\n_**Maximum Value:**_ 60\n\n_**Config Example:**_ `file_fragment_size = \"25\"`\n\n_**CLI Option Use:**_ `--file-fragment-size = '25'`\n\n> [!NOTE]\n> Microsoft OneDrive requires that the file fragment size be an exact multiple of 320 KiB. The default value is an exact multiple of this required value. Additional exact multiple options are:\n> 15, 20, 25, 30, 35, 40, 45, 50, 55\n\n\n### force_http_11\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `force_http_11 = \"false\"` or `force_http_11 = \"true\"`\n\n_**CLI Option Use:**_ `--force-http-11`\n\n\n### force_session_upload\n_**Description:**_ This option, when enabled, forces the client to use a 'session' upload, which, when the 'file' is uploaded by the session, this includes the local timestamp of the file.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `force_session_upload = \"false\"` or `force_session_upload = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n\n### inotify_delay\n_**Description:**_ This option specifies the number of seconds 'inotify' events are paused before they are processed by this client. This value is used to overcome aggressive write applications such as Obsidian which write each keystroke in an atomic manner to the local disk. Due to this atomic write, each 'save' causes the existing file to be deleted and replaced with a new file, which this client sees as multiple constant 'inotify' events.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 5\n\n_**Maximum Value:**_ 15\n\n_**Config Example:**_ `inotify_delay = \"10\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n> [!NOTE]\n> This option is only used if 'delay_inotify_processing' is enabled, otherwise this option is ignored.\n\n### ip_protocol_version\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 0\n\n_**Valid Values:**_ 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only\n\n_**Config Example:**_ `ip_protocol_version = \"0\"` or `ip_protocol_version = \"1\"` or `ip_protocol_version = \"2\"`\n\n> [!IMPORTANT]\n> 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.\n\n### local_first\n_**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'.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `local_first = \"false\"` or `local_first = \"true\"`\n\n_**CLI Option Use:**_ `--local-first`\n\n### log_dir\n_**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`.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ *None*\n\n_**Config Example:**_ `log_dir = \"~/logs/\"`\n\n_**CLI Option Use:**_ `--log-dir \"~/logs/\"`\n\n### max_curl_idle\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 120\n\n_**Config Example:**_ `max_curl_idle = \"120\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n> [!IMPORTANT]\n> 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.\n\n### monitor_fullscan_frequency\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 12\n\n_**Config Example:**_ `monitor_fullscan_frequency = \"24\"`\n\n_**CLI Option Use:**_ `--monitor-fullscan-frequency '24'`\n\n> [!NOTE]\n> 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.\n\n### monitor_interval\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 300\n\n_**Config Example:**_ `monitor_interval = \"600\"`\n\n_**CLI Option Use:**_ `--monitor-interval '600'`\n\n> [!NOTE]\n> A minimum value of 300 is enforced for this configuration setting.\n\n### monitor_log_frequency\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 12\n\n_**Config Example:**_ `monitor_log_frequency = \"24\"`\n\n_**CLI Option Use:**_ `--monitor-log-frequency '24'`\n\n_**Usage Example:**_ \n\nBy 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:\n```text\nReading configuration file: /home/user/.config/onedrive/config\nConfiguration file successfully loaded\nConfiguring Global Azure AD Endpoints\nSync Engine Initialised with new Onedrive API instance\nAll application operations will be performed in: /home/user/OneDrive\nOneDrive synchronisation interval (seconds): 300\nInitialising filesystem inotify monitoring ...\nPerforming initial synchronisation to ensure consistent local state ...\nStarting a sync with Microsoft OneDrive\nFetching items from the OneDrive API for Drive ID: b!bO8V6s9SSk9R7mWhpIjUrotN73WlW3tEv3OxP_QfIdQimEdOHR-1So6CqeG1MfDB ..\nProcessing changes and items received from Microsoft OneDrive ...\nPerforming a database consistency and integrity check on locally stored data ... \nScanning the local file system '~/OneDrive' for new data to upload ...\nPerforming a final true-up scan of online data from Microsoft OneDrive\nFetching items from the OneDrive API for Drive ID: b!bO8V6s9SSk9R7mWhpIjUrotN73WlW3tEv3OxP_QfIdQimEdOHR-1So6CqeG1MfDB ..\nProcessing changes and items received from Microsoft OneDrive ...\nSync with Microsoft OneDrive is complete\n```\nThen, based on 'monitor_log_frequency', the following output will be logged until the suppression loop value is reached:\n```text\nStarting a sync with Microsoft OneDrive\nSyncing changes from Microsoft OneDrive ...\nSync with Microsoft OneDrive is complete\n```\n> [!NOTE]\n> 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'.\n\n> [!NOTE]\n> 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.\n\n### no_remote_delete\n_**Description:**_ This configuration option controls whether local file and folder deletes are actioned on Microsoft OneDrive.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `local_first = \"false\"` or `local_first = \"true\"`\n\n_**CLI Option Use:**_ `--no-remote-delete`\n\n> [!IMPORTANT]\n> This configuration option can *only* be used in conjunction with `--upload-only`\n\n### notify_file_actions\n_**Description:**_ This configuration option controls whether the client will log via GUI notifications successful actions that the client performs.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `notify_file_actions = \"true\"`\n\n> [!NOTE]\n> GUI Notification Support must be compiled in first, otherwise this option will have zero effect and will not be used.\n\n### operation_timeout\n_**Description:**_ This configuration option controls the maximum total time (in seconds) that any network operation is allowed to take. This limit applies to the *entire* request, including DNS resolution, connection setup, TLS negotiation, and data transfer.  This option maps directly to libcurl’s `CURLOPT_TIMEOUT`.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 0 (no timeout)\n\n_**Config Example:**_ `operation_timeout = \"3600\"`\n\n> [!IMPORTANT]\n> Setting a non-zero value will cause libcurl to abort the operation once the specified time has elapsed — even if data is still flowing normally.\n> For large file downloads, particularly on slower connections, enabling a finite timeout may cause transfers to be terminated prematurely.\n>\n> It is strongly recommend to leave this option at its default of `0` unless you specifically require a hard global time limit.\n\n### permanent_delete\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `permanent_delete = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n> [!IMPORTANT]\n> The Microsoft OneDrive API for this capability is also very narrow:\n> | Account Type | Config Option is Supported |\n> |:-------------|:----------------:|\n> | Personal     | ❌ |\n> | Business     | ✔ |\n> | SharePoint   | ✔ |\n> | Microsoft Cloud Germany | ✔ |\n> | Microsoft Cloud for US Government | ❌ |\n> | Azure and Office365 operated by VNET in China | ❌ |\n> \n> When using this config option against an unsupported Personal Accounts the following message will be generated:\n> ```\n> WARNING: The application is configured to permanently delete files online; however, this action is not supported by Microsoft OneDrive Personal Accounts.\n> ```\n> \n> When using this config option against a supported account the following message will be generated:\n> ```\n> WARNING: Application has been configured to permanently remove files online rather than send to the recycle bin. Permanently deleted items can't be restored.\n> WARNING: Online data loss MAY occur in this scenario.\n> ```\n>\n\n### rate_limit\n_**Description:**_ This configuration option controls the bandwidth used by the application, per thread, when interacting with Microsoft OneDrive.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 0 (unlimited, use available bandwidth per thread)\n\n_**Valid Values:**_ Valid tested values for this configuration option are as follows:\n\n* 131072 \t= 128 KB/s - absolute minimum for basic application operations to prevent timeouts\n* 262144 \t= 256 KB/s\n* 524288\t= 512 KB/s\n* 1048576 \t= 1 MB/s\n* 10485760 \t= 10 MB/s\n* 104857600 = 100 MB/s\n\n_**Config Example:**_ `rate_limit = \"131072\"`\n\n### read_only_auth_scope\n_**Description:**_ This configuration option controls whether the OneDrive Client for Linux operates in a totally in read-only operation.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `read_only_auth_scope = \"false\"` or `read_only_auth_scope = \"true\"`\n\n> [!IMPORTANT]\n> 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.\n\n### recycle_bin_path\n_**Description:**_ This configuration option allows you to specify the 'Recycle Bin' path for the application.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ *None* however the application will use `~/.local/share/Trash` as the pre-defined default so that files will be placed in the correct location for your user profile.\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n_**Config Example:**_ `recycle_bin_path = \"/path/to/desired/location/\"`\n\n### remove_source_files\n_**Description:**_ This configuration option controls whether the OneDrive Client for Linux removes the local file post successful transfer to Microsoft OneDrive.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `remove_source_files = \"false\"` or `remove_source_files = \"true\"`\n\n_**CLI Option Use:**_ `--remove-source-files`\n\n> [!IMPORTANT]\n> This configuration option can *only* be used in conjunction with `--upload-only`\n\n### remove_source_folders\n_**Description:**_ This configuration option controls whether the OneDrive Client for Linux removes the local directory structure post successful file transfer to Microsoft OneDrive.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `remove_source_folders = \"false\"` or `remove_source_folders = \"true\"`\n\n_**CLI Option Use:**_ `--remove-source-folders`\n\n> [!IMPORTANT]\n> This configuration option can *only* be used in conjunction with `--upload-only --remove-source-files`\n\n> [!IMPORTANT]\n> The directory structure will only be removed if it is empty.\n\n### resync\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `resync = \"false\"` or `resync = \"true\"`\n\n_**CLI Option Use:**_ `--resync`\n\n> [!CAUTION]\n> 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:\n> *   drive_id\n> *   sync_dir\n> *   skip_file\n> *   skip_dir\n> *   skip_dotfiles\n> *   skip_symlinks\n> *   sync_business_shared_items\n> *   Creating, Modifying or Deleting the 'sync_list' file\n\n> [!IMPORTANT]\n> The increased activity against the Microsoft Graph API when using this option may trigger HTTP 429 (throttling) responses during the synchronisation process.\n\n### resync_auth\n_**Description:**_ This configuration option controls the approval of performing a 'resync' which can be beneficial in automated environments.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `resync_auth = \"false\"` or `resync_auth = \"true\"`\n\n_**CLI Option Use:**_ `--resync-auth`\n\n> [!TIP]\n> 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.\n\n### skip_dir\n_**Description:**_ This configuration option controls whether the application skips certain directories from being synced. Directories can be specified in 2 ways:\n\n* 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.\n* As a full path entry. This will skip the explicit path as set.\n\n> [!IMPORTANT]\n> Entries for 'skip_dir' are *relative* to your 'sync_dir' path.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ *Empty* - not required for normal operation\n\n_**Config Example:**_ \n\nPatterns are case insensitive. `*` and `?` [wildcards characters](https://technet.microsoft.com/en-us/library/bb490639.aspx) are supported. Use `|` to separate multiple patterns. \n\n```text\nskip_dir = \"Desktop|Documents/IISExpress|Documents/SQL Server Management Studio|Documents/Visual Studio*|Documents/WindowsPowerShell|.Rproj-user\"\n```\n\nThe 'skip_dir' option can also be specified multiple times within your config file, for example:\n```text\nskip_dir = \"SkipThisDirectoryAnywhere\"\nskip_dir = \".SkipThisOtherDirectoryAnywhere\"\nskip_dir = \"/Explicit/Path/To/A/Directory\"\nskip_dir = \"/Another/Explicit/Path/To/Different/Directory\"\n```\n\nThis will be interpreted the same as:\n```text\nskip_dir = \"SkipThisDirectoryAnywhere|.SkipThisOtherDirectoryAnywhere|/Explicit/Path/To/A/Directory|/Another/Explicit/Path/To/Different/Directory\"\n```\n\n_**CLI Option Use:**_ `--skip-dir 'SkipThisDirectoryAnywhere|.SkipThisOtherDirectoryAnywhere|/Explicit/Path/To/A/Directory|/Another/Explicit/Path/To/Different/Directory'`\n\n> [!NOTE]\n> 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.\n\n### skip_dir_strict_match\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `skip_dir_strict_match = \"false\"` or `skip_dir_strict_match = \"true\"`\n\n_**CLI Option Use:**_ `--skip-dir-strict-match`\n\n### skip_dotfiles\n_**Description:**_ This configuration option controls whether the application will skip all .files and .folders when performing sync operations.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `skip_dotfiles = \"false\"` or `skip_dotfiles = \"true\"`\n\n_**CLI Option Use:**_ `--skip-dot-files`\n\n> [!NOTE]\n> 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.\n\n### skip_file\n_**Description:**_ This configuration option controls whether the application skips certain files from being synced.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ `~*|.~*|*.tmp|*.swp|*.partial`\n\nBy default, the following files will be skipped:\n\n| Skip File Pattern | Meaning                    | Why this should be skipped |\n|:------------------|:---------------------------|:---------------------------|\n| `~*`              | Files that start with `~`  | Temporary or backup files. Typically auto-created by various programs during editing sessions. These are not intended to be saved permanently. Example: Emacs, Vim, and others create such files. |\n| `.~*`             | Files that start with `.~` | Hidden lock or temp files, especially from LibreOffice and OpenOffice. (E.g., `.~lock.MyFile.docx#`) These are only used to prevent multiple users editing the same file simultaneously. |\n| `*.tmp`           | Files ending in `.tmp`     | Generic temporary files created by applications like browsers, editors, installers. They represent intermediate data and are usually auto-deleted after a session. |\n| `*.swp`           | Files ending in `.swp`     | Vim (and vi) swap files. Created to protect against crash recovery during text editing. Should not be synced because they are transient. |\n| `*.partial`       | Files ending in `.partial` | Partially downloaded files. Common in browsers (like Firefox `.partial` download files), background downloaders and this client. Incomplete by nature. Syncing them causes broken files online. |\n\nThe following suggested skip file patterns are not included in the default configuration but could also be considered for skipping:\n\n| Skip File Pattern | Meaning                    | Why this should be skipped |\n|:------------------|:---------------------------|:---------------------------|\n| `*.bak`           | Files ending in `.bak`     | Backup files created by many text editors, IDEs, or applications. These are automatic backups made to preserve earlier versions of files before editing changes are saved. They are not intended for syncing — they are redundant copies of existing or previous files. |\n\n> [!IMPORTANT]\n> If you define your own 'skip_file' configuration, the default settings listed above will be *overridden*. It is strongly recommended that you explicitly include the default 'skip_file' rules alongside your custom entries to ensure temporary and/or transient files are still correctly skipped.\n\n_**Config Example:**_ \n\nPatterns are case insensitive. `*` and `?` [wildcards characters](https://technet.microsoft.com/en-us/library/bb490639.aspx) are supported. Use `|` to separate multiple patterns.\n\nFiles can be skipped in the following fashion:\n*   Specify a wildcard, eg: '*.txt' (skip all txt files)\n*   Explicitly specify the filename and it's full path relative to your sync_dir, eg: '/path/to/file/filename.ext'\n*   Explicitly specify the filename only and skip every instance of this filename, eg: 'filename.ext'\n\n```text\nskip_file = \"~*|/Documents/OneNote*|/Documents/config.xlaunch|myfile.ext|/Documents/keepass.kdbx\"\n```\n\n> [!IMPORTANT]\n> Entries for 'skip_file' are *relative* to your 'sync_dir' path.\n\nThe 'skip_file' option can be specified multiple times within your config file, for example:\n```text\n# Defaults - always keep\nskip_file = \"~*|.~*|*.tmp|*.swp|*.partial\"\n# Custom 'skip_file' additions\nskip_file = \"*.blah\"\nskip_file = \"never_sync.file\"\nskip_file = \"/Documents/keepass.kdbx\"\n```\nThis will be interpreted the same as:\n```text\nskip_file = \"~*|.~*|*.tmp|*.swp|*.partial|*.blah|never_sync.file|/Documents/keepass.kdbx\"\n```\n\n_**CLI Option Use:**_ `--skip-file '~*|.~*|*.tmp|*.swp|*.partial|*.blah|never_sync.file|/Documents/keepass.kdbx'`\n\n> [!NOTE]\n> 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.\n\n### skip_size\n_**Description:**_ This configuration option controls whether the application skips syncing certain files larger than the specified size. The value specified is in MB.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 0 (all files, regardless of size, are synced)\n\n_**Config Example:**_ `skip_size = \"50\"`\n\n_**CLI Option Use:**_ `--skip-size '50'`\n\n> [!NOTE]\n> 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.\n\n\n### skip_symlinks\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `skip_symlinks = \"false\"` or `skip_symlinks = \"true\"`\n\n_**CLI Option Use:**_ `--skip-symlinks`\n\n> [!NOTE]\n> 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.\n\n### space_reservation\n_**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\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 50 MB (expressed as Bytes when using `--display-config`)\n\n_**Config Example:**_ `space_reservation = \"100\"`\n\n_**CLI Option Use:**_ `--space-reservation '100'`\n\n### sync_business_shared_items\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `sync_business_shared_items = \"false\"` or `sync_business_shared_items = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n> [!NOTE]\n> 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.\n\n> [!CAUTION]\n> 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.\n\n### sync_dir\n_**Description:**_ This configuration option determines the location on your local filesystem where your data from Microsoft OneDrive will be saved.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ `~/OneDrive`\n\n_**Config Example:**_ `sync_dir = \"~/MyDirToSync\"`\n\n_**CLI Option Use:**_ `--syncdir '~/MyDirToSync'`\n\n> [!CAUTION]\n> 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.\n\n### sync_dir_permissions\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ `700` - This provides the following permissions: `drwx------`\n\n_**Config Example:**_ `sync_dir_permissions = \"700\"`\n\n> [!IMPORTANT]\n> 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.\n\n### sync_file_permissions\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ `600` - This provides the following permissions: `-rw-------`\n\n_**Config Example:**_ `sync_file_permissions = \"600\"`\n\n> [!IMPORTANT]\n> 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.\n\n### sync_root_files\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `sync_root_files = \"false\"` or `sync_root_files = \"true\"`\n\n_**CLI Option Use:**_ `--sync-root-files`\n\n> [!IMPORTANT]\n> 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.\n\n### threads\n_**Description:**_ This configuration option controls the number of worker threads used for parallel upload and download operations when transferring files between your local system and Microsoft OneDrive. Each thread handles a discrete portion of the workload, improving performance when used appropriately. All non-transfer operations, such as folder listings (`/children`), delta queries (`/delta`), and metadata requests are processed serially on a single thread.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ `8`\n\n_**Maximum Value:**_ `16`\n\n_**Config Example:**_ `threads = \"16\"`\n\n_**CLI Option Use:**_ `--threads '16'`\n\n> [!NOTE]\n> The default value of `8` threads is based on the average number of physical CPU cores found in consumer and workstation-grade Intel and AMD processors released from approximately 2012 through 2025. This includes laptops, desktops, and server-grade CPUs where 4–8 physical cores are typical.\n> \n> In extensive testing, configuring the application with more than `16` threads — regardless of available physical CPU cores — frequently caused the Microsoft OneDrive service to become blocked due to excessive API request volume.\n\n> [!NOTE]\n> The threads setting only affects file transfer operations. All API operations outside of upload/download operations are single-threaded.\n>\n> This option allows the alignment to Microsoft’s [Graph API guidance](https://learn.microsoft.com/en-us/graph/throttling) which recommends limiting concurrent requests to 5–10. The default of `8` provides a safe and performant baseline.\n\n> [!IMPORTANT]\n> For optimal performance and application stability, the number of threads should not exceed the number of **physical CPU cores** available to the system. Setting the thread count too high can result in **CPU contention**, increased **context switching**, and **reduced throughput** due to over-scheduling.\n>\n> If running inside a container or virtual machine, ensure that the container/VM has sufficient allocated CPU cores before increasing this setting.\n\n> [!IMPORTANT]\n> If the configured `threads` value (default or manual) exceeds the number of available CPU cores, the application will issue a warning similar to the following:\n>  \n> ```\n> WARNING: Configured 'threads = 8' exceeds available CPU cores (CPU_COUNT).\n>          This may lead to reduced performance, CPU contention, and instability. For best results, set 'threads' no higher than the number of physical CPU cores.\n> ```\n>\n> If this warning message appears during application startup, you **must** review and adjust your threads setting to match the number of physical CPU cores on your system to avoid degraded performance or instability.\n\n> [!IMPORTANT]\n> The application fully implements Microsoft’s throttling requirements for handling 429 and 503 response codes by:\n> * Handles 429 and 503 responses using exponential backoff\n> * Respects Retry-After headers provided by the API for the required back off period\n> * Limits concurrency to the recommended limits\n> \n> If you receive this application output:\n>```\n>Handling a Microsoft Graph API HTTP 429 Response Code (Too Many Requests) - Internal Thread ID: AbCdEfGhIjKlMnOp\n>```\n> Reduce your configured 'threads' value or raise a support ticket with Microsoft\n\n\n> [!WARNING]\n> Increasing or keeping the thread count beyond the default or available physical CPU cores will also result in higher **system resource utilisation**, particularly in terms of CPU load and local TCP port consumption. On lower-spec systems or in constrained environments, this may lead to **network saturation**, **unpredictable behaviour**, **increase in throttling behaviour by Microsoft** or **application crashes** due to resource exhaustion.\n\n\n### transfer_order\n_**Description:**_ This configuration option controls the transfer order of files between your local system and Microsoft OneDrive.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ `default`\n\n_**Config Example:**_\n#### Transfer by size, smallest first\n```\ntransfer_order = \"size_asc\"\n```\n\n#### Transfer by size, largest first\n```\ntransfer_order = \"size_dsc\"\n```\n#### Transfer by file name sorted A to Z\n```\ntransfer_order = \"name_asc\"\n```\n#### Transfer by file name sorted Z to A\n```\ntransfer_order = \"name_dsc\"\n```\n\n### upload_only\n_**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.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `upload_only = \"false\"` or `upload_only = \"true\"`\n\n_**CLI Option Use:**_ `--upload-only`\n\n> [!IMPORTANT]\n> 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.\n\n### use_device_auth\n_**Description:**_ Enable this option to authenticate using the Microsoft OAuth2 Device Authorisation Flow (`device_code` grant). This flow allows the client to initiate a sign-in process without launching a web browser directly — ideal for headless systems or remote sessions. A short code and URL will be provided for the user to complete authentication via a separate browser-enabled device.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `use_device_auth = \"false\"` or `use_device_auth = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n> [!IMPORTANT]\n> This option is fully supported for Microsoft Entra ID (Work/School) accounts. For personal Microsoft accounts (e.g., @outlook.com or @hotmail.com), this method of authentication is not supported. Please use the interactive interactive authentication method (default) to authenticate this application.\n\n### use_intune_sso\n_**Description:**_ Enable this option to authenticate using Intune Single Sign-On (SSO) via the Microsoft Identity Device Broker over D-Bus. This method is suitable for environments where the system is Intune-enrolled and allows seamless token retrieval without requiring browser interaction.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `use_intune_sso = \"false\"` or `use_intune_sso = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n> [!NOTE]\n> The installation and configuration of Intune for your platform is beyond the scope of this documentation.\n\n### use_recycle_bin\n_**Description:**_ This configuration option controls the application function to move online deleted files to a 'Recycle Bin' on your system. This allows you to review online deleted data manually before this is purged from your actual system.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `use_recycle_bin = \"false\"` or `use_recycle_bin = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n### user_agent\n_**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.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ `ISV|abraunegg|OneDrive Client for Linux/vX.Y.Z-A-bcdefghi`\n\n_**Config Example:**_ `user_agent = \"ISV|CompanyName|AppName/Version\"`\n\n> [!IMPORTANT]\n> 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\n\n### webhook_enabled\n_**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'.\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ The following is the minimum working example that needs to be added to your 'config' file to enable 'webhooks' successfully:\n```text\nwebhook_enabled = \"true\"\nwebhook_public_url = \"https://<your.fully.qualified.domain.name>/webhooks/onedrive\"\n```\n\n> [!NOTE]\n> 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`.\n\n> [!IMPORTANT]\n> 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.\n\n> [!TIP]\n> 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.\n> \n> To resolve this configuration issue, potentially investigate adding the following 'proxy' configuration options to your nginx configuration file:\n> ```text\n> server {\n> \tlisten 443;\n>\tserver_name <your.fully.qualified.domain.name>;\n> \tlocation /webhooks/onedrive {\n> \t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n> \t\tproxy_set_header X-Original-Request-URI $request_uri;\n> \t\tproxy_read_timeout 300s;\n> \t\tproxy_connect_timeout 75s;\n> \t\tproxy_buffering off;\n> \t\tproxy_http_version 1.1;\n> \t\tproxy_pass http://127.0.0.1:8888;\n> \t}\n> }\n> ```\n> For any further nginx configuration assistance, please refer to: https://docs.nginx.com/\n\n### webhook_expiration_interval\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 600 \n\n_**Config Example:**_ `webhook_expiration_interval = \"1200\"`\n\n### webhook_listening_host\n_**Description:**_ This configuration option controls the host address that this client binds to, when the webhook feature is enabled.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ 0.0.0.0\n\n_**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.\n\n> [!NOTE]\n> Use in conjunction with 'webhook_listening_port' to change the webhook listening endpoint.\n\n### webhook_listening_port\n_**Description:**_ This configuration option controls the TCP port that this client listens on, when the webhook feature is enabled.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 8888\n\n_**Config Example:**_ `webhook_listening_port = \"9999\"`\n\n> [!NOTE]\n> Use in conjunction with 'webhook_listening_host' to change the webhook listening endpoint.\n\n### webhook_public_url\n_**Description:**_ This configuration option controls the URL that Microsoft will send subscription notifications to. This must be a valid Internet accessible URL.\n\n_**Value Type:**_ String\n\n_**Default Value:**_ *empty*\n\n_**Config Example:**_ \n```text\nwebhook_public_url = \"https://<your.fully.qualified.domain.name>/webhooks/onedrive\"\n```\n\n### webhook_renewal_interval\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 300\n\n_**Config Example:**_ `webhook_renewal_interval = \"600\"`\n\n### webhook_retry_interval\n_**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.\n\n_**Value Type:**_ Integer\n\n_**Default Value:**_ 60\n\n_**Config Example:**_ `webhook_retry_interval = \"120\"`\n\n### write_xattr_data\n_**Description:**_ This setting enables writing xattr values detailing the 'createdBy' and 'lastModifiedBy' information provided by the OneDrive API\n\n_**Value Type:**_ Boolean\n\n_**Default Value:**_ False\n\n_**Config Example:**_ `write_xattr_data = \"false\"` or `write_xattr_data = \"true\"`\n\n_**CLI Option Use:**_ *None - this is a config file option only*\n\n_**xattr Data Example:**_\n```\nuser.onedrive.createdBy=\"Account Display Name\"\nuser.onedrive.lastModifiedBy=\"Account Display Name\"\n```\n\n\n## Command Line Interface (CLI) Only Options\n\n### CLI Option: --auth-files\n_**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.\n\n_**Usage Example:**_ `onedrive --auth-files authUrl:responseUrl`\n\n> [!IMPORTANT]\n> 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:\n> \n> ```text\n> onedrive --auth-files '~/onedrive-auth-url:~/onedrive-response-url' \n> Reading configuration file: /home/alex/.config/onedrive/config\n> Configuration file successfully loaded\n> Configuring Global Azure AD Endpoints\n> Client requires authentication before proceeding. Waiting for --auth-files elements to be available.\n> ```\n> 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`.\n> \n> 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.\n> ```text\n> Sync Engine Initialised with new Onedrive API instance\n> \n>  --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.\n> \n> No OneDrive sync will be performed without one of these two arguments being present.\n> ```\n\n### CLI Option: --auth-response\n_**Description:**_ This CLI option allows the user to perform application authentication not via an interactive dialog but via providing the authentication response URI directly.\n\n_**Usage Example:**_ `onedrive --auth-response https://login.microsoftonline.com/common/oauth2/nativeclient?code=<redacted>`\n\n> [!TIP]\n> 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:\n> ```text\n> 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 \n> ```\n> 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.\n\n### CLI Option: --confdir\n_**Description:**_ This CLI option allows the user to specify where all the application configuration and relevant components are stored.\n\n_**Usage Example:**_ `onedrive --confdir '~/.config/onedrive-business/'`\n\n> [!IMPORTANT]\n> 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.\n\n### CLI Option: --create-directory\n_**Description:**_ This CLI option allows the user to create the specified directory path on Microsoft OneDrive without performing a sync.\n\n_**Usage Example:**_ `onedrive --create-directory 'path/of/new/folder/structure/to/create/'`\n\n> [!IMPORTANT]\n> The specified path to create is relative to your configured 'sync_dir'.\n\n### CLI Option: --create-share-link\n_**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'.\n\n_**Usage Example:**_ `onedrive --create-share-link 'relative/path/to/your/file.txt'`\n\n> [!IMPORTANT]\n> If writable access to the file is required, you must add `--with-editing-perms` to your command. See below for details.\n\n### CLI Option: --destination-directory\n_**Description:**_ This CLI option specifies the 'destination' portion of moving a file or folder online, without performing a sync operation.\n\n_**Usage Example:**_ `onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'`\n\n> [!IMPORTANT]\n> All specified paths are relative to your configured 'sync_dir'.\n\n### CLI Option: --display-config\n_**Description:**_ This CLI option will display the effective application configuration\n\n_**Usage Example:**_ `onedrive --display-config`\n\n### CLI Option: --display-sync-status\n_**Description:**_ This CLI option will display the sync status of the configured 'sync_dir'\n\n_**Usage Example:**_ `onedrive --display-sync-status`\n\n> [!TIP]\n> This option can also use the `--single-directory` option to determine the sync status of a specific directory within the configured 'sync_dir'\n\n### CLI Option: ---display-quota\n_**Description:**_ This CLI option will display the quota status of the account drive id or the configured 'drive_id' value\n\n_**Usage Example:**_ `onedrive --display-quota`\n\n### CLI Option: --download-file\n_**Description:**_ This CLI option will download a single file based on the online path. No sync will be performed.\n\n_**Usage Example:**_ `onedrive --download-file 'path/to/your/file/online'`\n\n### CLI Option: --force\n_**Description:**_ This CLI option enables the force the deletion of data when a 'big delete' is detected. \n\n_**Usage Example:**_ `onedrive --sync --verbose --force`\n\n> [!IMPORTANT]\n> 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.\n\n### CLI Option: --force-sync\n_**Description:**_ This CLI option enables the syncing of a specific directory, using the Client Side Filtering application defaults, overriding any user application configuration.\n\n_**Usage Example:**_ `onedrive --sync --verbose --force-sync --single-directory 'Data'`\n\n> [!NOTE]\n> When this option is used, you will be presented with the following warning and risk acceptance:\n> ```text\n> WARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --synch --single-directory --force-sync being used\n> \n> The use of --force-sync will reconfigure the application to use defaults. This may have untold and unknown future impacts.\n> By proceeding in using this option you accept any impacts including any data loss that may occur as a result of using --force-sync.\n> \n> Are you sure you wish to proceed with --force-sync [Y/N] \n> ```\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.\n\n### CLI Option: --get-file-link\n_**Description:**_ This CLI option queries the OneDrive API and return's the WebURL for the given local file.\n\n_**Usage Example:**_ `onedrive --get-file-link 'relative/path/to/your/file.txt'`\n\n> [!IMPORTANT]\n> The path that you should use *must* be relative to your 'sync_dir'\n\n### CLI Option: --get-sharepoint-drive-id\n_**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.\n\n_**Usage Example:**_ `onedrive --get-sharepoint-drive-id '*'` or `onedrive --get-sharepoint-drive-id 'PointPublishing Hub Site'`\n\n### CLI Option: --list-shared-items\n_**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.\n\n_**Usage Example:**_ `onedrive --list-shared-items`\n\n_**Example Output:**_\n```\n...\nListing available OneDrive Business Shared Items:\n\n-----------------------------------------------------------------------------------\nShared File:     large_document_shared.docx\nShared By:       test user (testuser@domain.tld)\n-----------------------------------------------------------------------------------\nShared File:     no_download_access.docx\nShared By:       test user (testuser@domain.tld)\n-----------------------------------------------------------------------------------\nShared File:     online_access_only.txt\nShared By:       test user (testuser@domain.tld)\n-----------------------------------------------------------------------------------\nShared File:     read_only.txt\nShared By:       test user (testuser@domain.tld)\n-----------------------------------------------------------------------------------\nShared File:     qewrqwerwqer.txt\nShared By:       test user (testuser@domain.tld)\n-----------------------------------------------------------------------------------\nShared File:     dummy_file_to_share.docx\nShared By:       testuser2 testuser2 (testuser2@domain.tld)\n-----------------------------------------------------------------------------------\nShared Folder:   Sub Folder 2\nShared By:       test user (testuser@domain.tld)\n-----------------------------------------------------------------------------------\nShared File:     file to share.docx\nShared By:       test user (testuser@domain.tld)\n-----------------------------------------------------------------------------------\nShared Folder:   Top Folder\nShared By:       test user (testuser@domain.tld)\n-----------------------------------------------------------------------------------\n...\n```\n\n### CLI Option: --logout\n_**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.\n\n_**Usage Example:**_ `onedrive --logout`\n\n### CLI Option: --modified-by\n_**Description:**_ This CLI option queries the OneDrive API and return's the last modified details for the given local file.\n\n_**Usage Example:**_ `onedrive --modified-by 'relative/path/to/your/file.txt'`\n\n> [!IMPORTANT]\n> The path that you should use *must* be relative to your 'sync_dir'\n\n### CLI Option: --monitor | -m\n_**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.\n\n_**Usage Example:**_ `onedrive --monitor` or `onedrive -m`\n\n### CLI Option: --print-access-token\n_**Description:**_ Print the current access token being used to access Microsoft OneDrive. \n\n_**Usage Example:**_ `onedrive --verbose --verbose --debug-https --print-access-token`\n\n> [!CAUTION]\n> 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.\n\n### CLI Option: --reauth\n_**Description:**_ This CLI option controls the ability to re-authenticate your client with Microsoft OneDrive.\n\n_**Usage Example:**_ `onedrive --reauth`\n\n### CLI Option: --remove-directory\n_**Description:**_ This CLI option allows the user to remove the specified directory path on Microsoft OneDrive without performing a sync.\n\n_**Usage Example:**_ `onedrive --remove-directory 'path/of/new/folder/structure/to/remove/'`\n\n> [!IMPORTANT]\n> The specified path to remove is relative to your configured 'sync_dir'.\n\n### CLI Option: --share-password\n_**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`\n\n_**Usage Example:**_ `onedrive --create-share-link 'relative/path/to/your/file.txt' --share-password 'valid password'`\n\n### CLI Option: --single-directory\n_**Description:**_ This CLI option controls the applications ability to sync a specific single directory.\n\n_**Usage Example:**_ `onedrive --sync --single-directory 'Data'`\n\n> [!IMPORTANT]\n> 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'`.\n\n### CLI Option: --source-directory\n_**Description:**_ This CLI option specifies the 'source' portion of moving a file or folder online, without performing a sync operation.\n\n_**Usage Example:**_ `onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'`\n\n> [!IMPORTANT]\n> All specified paths are relative to your configured 'sync_dir'.\n\n### CLI Option: --sync | -s\n_**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.\n\n_**Usage Example:**_ `onedrive --sync` or `onedrive -s`\n\n### CLI Option: --sync-shared-files\n_**Description:**_ Sync OneDrive Business Shared Files to the local filesystem.\n\n_**Usage Example:**_ `onedrive --sync --sync-shared-files`\n\n> [!IMPORTANT]\n> 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.\n\n### CLI Option: --verbose | -v+\n_**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.\n\n_**Usage Example:**_ `onedrive --sync --verbose` or `onedrive --monitor --verbose`\n\n### CLI Option: --with-editing-perms\n_**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`\n\n_**Usage Example:**_ `onedrive --create-share-link 'relative/path/to/your/file.txt' --with-editing-perms`\n\n> [!IMPORTANT]\n> Placement of `--with-editing-perms` is critical. It *must* be placed after the file path as per the example above.\n\n## Deprecated Configuration File and CLI Options\nThe following configuration options are no longer supported:\n\n### force_http_2\n_**Description:**_ Force the use of HTTP/2 for all operations where applicable\n\n_**Deprecated Config Example:**_ `force_http_2 = \"true\"`\n\n_**Deprecated CLI Option:**_ `--force-http-2`\n\n_**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.\n\n### min_notify_changes\n_**Description:**_ Minimum number of pending incoming changes necessary to trigger a GUI desktop notification.\n\n_**Deprecated Config Example:**_ `min_notify_changes = \"50\"`\n\n_**Deprecated CLI Option:**_ `--min-notify-changes '50'`\n\n_**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.\n\n### CLI Option: --synchronize\n_**Description:**_ Perform a synchronisation with Microsoft OneDrive\n\n_**Deprecated CLI Option:**_ `--synchronize`\n\n_**Reason for depreciation:**_ `--synchronize` has been deprecated in favour of `--sync` or `-s`\n"
  },
  {
    "path": "docs/application-security.md",
    "content": "# OneDrive Client for Linux Application Security\nThis document details the following information:\n\n* Why is this application an 'unverified publisher'?\n* Application Security and Permission Scopes\n* How to change Permission Scopes\n* How to review your existing application access consent\n\n## Why is this application an 'unverified publisher'?\nPublisher 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!\n\n### Verified Publisher Configuration Evidence\nAs per the image below, the Azure portal shows that the 'Publisher Domain' has actually been verified:\n![confirmed_verified_publisher](./images/confirmed_verified_publisher.jpg)\n\n* The 'Publisher Domain' is: https://abraunegg.github.io/\n* The required 'Microsoft Identity Association' is: https://abraunegg.github.io/.well-known/microsoft-identity-association.json\n\n## Application Security and Permission Scopes\nThere are 2 main components regarding security for this application:\n* Azure Application Permissions\n* User Authentication Permissions\n\nKeeping this in mind, security options should follow the security principal of 'least privilege':\n> The principle that a security architecture should be designed so that each entity \n> is granted the minimum system resources and authorizations that the entity needs \n> to perform its function.\n\nReference: [https://csrc.nist.gov/glossary/term/least_privilege](https://csrc.nist.gov/glossary/term/least_privilege)\n\nAs such, the following API permissions are used by default:\n\n### Default Azure Application Permissions\n\n| API / Permissions name | Type | Description | Admin consent required |\n|---|---|---|---|\n| Files.Read | Delegated | Have read-only access to user files | No |\n| Files.Read.All  | Delegated | Have read-only access to all files user can access | No |\n| Sites.Read.All   | Delegated | Have read-only access to all items in all site collections | No |\n| offline_access   | Delegated | Maintain access to data you have given it access to | No |\n\n![default_authentication_scopes](./images/default_authentication_scopes.jpg)\n\n### Default User Authentication Permissions\n\nWhen 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:\n\n| API / Permissions name | Type | Description | Admin consent required |\n|---|---|---|---|\n| Files.ReadWrite | Delegated | Have full access to user files | No |\n| Files.ReadWrite.All  | Delegated | Have full access to all files user can access | No |\n| Sites.ReadWrite.All   | Delegated | Have full access to all items in all site collections | No |\n| offline_access   | Delegated | Maintain access to data you have given it access to | No |\n\nWhen 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:\n\n| API / Permissions name | Type | Description | Admin consent required |\n|---|---|---|---|\n| Files.ReadWrite | Delegated | Have full access to user files | No |\n| Files.ReadWrite.All  | Delegated | Have full access to all files user can access | No |\n| Sites.ReadWrite.All   | Delegated | Have full access to all items in all site collections | No |\n| offline_access   | Delegated | Maintain access to data you have given it access to | No |\n\nThese 'default' permissions will allow the OneDrive Client for Linux to read, write and delete data associated with your OneDrive Account.\n\n## How are the Authentication Scopes used?\n\nWhen 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:\n\n![Linux Authentication to Microsoft OneDrive](./puml/onedrive_linux_authentication.png)\n\nThis is similar to the Microsoft Windows OneDrive Client:\n\n![Windows Authentication to Microsoft OneDrive](./puml/onedrive_windows_authentication.png)\n\nIn 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.\n\n## Configuring read-only access to your OneDrive data\nIn some situations, it may be desirable to configure the OneDrive Client for Linux totally in read-only operation.\n\nTo change the application to 'read-only' access, add the following to your configuration file:\n```text\nread_only_auth_scope = \"true\"\n```\nThis will change the user authentication scope request to use read-only access.\n\n> [!IMPORTANT]\n> When changing this value, you *must* re-authenticate the client using the `--reauth` option to utilise the change in authentication scopes.\n\nWhen using read-only authentication scopes, the uploading of any data or local change to OneDrive will fail with the following error:\n```\n2022-Aug-06 13:16:45.3349625    ERROR: Microsoft OneDrive API returned an error with the following message:\n2022-Aug-06 13:16:45.3351661      Error Message:    HTTP request returned status code 403 (Forbidden)\n2022-Aug-06 13:16:45.3352467      Error Reason:     Access denied\n2022-Aug-06 13:16:45.3352838      Error Timestamp:  2022-06-12T13:16:45\n2022-Aug-06 13:16:45.3353171      API Request ID:   <redacted>\n```\n\nAs such, it is also advisable for you to add the following to your configuration file so that 'uploads' are prevented:\n```text\ndownload_only = \"true\"\n```\n\n> [!IMPORTANT]\n> 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.\n \n## Reviewing your existing application access consent\n\nTo review your existing application access consent, you need to access the following URL: https://account.live.com/consent/Manage\n\nFrom here, you are able to review what applications have been given what access to your data, and remove application access as required.\n"
  },
  {
    "path": "docs/build-rpm-howto.md",
    "content": "# RPM Package Build Process\nThe instructions below have been tested on the following systems:\n*   CentOS Stream release 9\n\nThese instructions should also be applicable for RedHat & Fedora platforms, or any other RedHat RPM based distribution.\n\n## Prepare Package Development Environment\n\n### Install Development Dependencies\nInstall the following dependencies on your build system:\n```text\nsudo yum groupinstall -y 'Development Tools'\nsudo yum install -y libcurl-devel\nsudo yum install -y sqlite-devel\nsudo yum install -y libnotify-devel\nsudo yum install -y dbus-devel\nsudo yum install -y wget\nmkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}\n```\n\n### Install DMD Compiler for Linux\nInstall the latest DMD Compiler for Linux from https://dlang.org/download.html using the Fedora/CentOS x86_64 link.\n\nIllustrated below is the installation using the minimum supported compiler. You should always install the latest version of the compiler for your platform when manually building an RPM.\n```text\nsudo yum install -y https://downloads.dlang.org/releases/2.x/2.091.1/dmd-2.091.1-0.fedora.x86_64.rpm\n```\n\n## Build RPM from spec file using the DMD Compiler\nBuild the RPM from the provided spec file:\n```text\nwget https://github.com/abraunegg/onedrive/archive/refs/tags/v2.5.6.tar.gz -O ~/rpmbuild/SOURCES/v2.5.6.tar.gz\nwget https://raw.githubusercontent.com/abraunegg/onedrive/master/contrib/spec/onedrive.spec.in -O ~/rpmbuild/SPECS/onedrive.spec\nrpmbuild -ba ~/rpmbuild/SPECS/onedrive.spec --define 'dcompiler dmd'\n```\n\n### RPM Build Example Results\nBelow are example output results of building, installing and running the RPM package on the respective platforms:\n\n#### CentOS Stream release 9 RPM Build Process\n```text\nsetting SOURCE_DATE_EPOCH=1749081600\nExecuting(%prep): /bin/sh -e /var/tmp/rpm-tmp.ZhVuOR\n+ umask 022\n+ cd /home/alex/rpmbuild/BUILD\n+ cd /home/alex/rpmbuild/BUILD\n+ rm -rf onedrive-2.5.6\n+ /usr/bin/tar -xof -\n+ /usr/bin/gzip -dc /home/alex/rpmbuild/SOURCES/v2.5.6.tar.gz\n+ STATUS=0\n+ '[' 0 -ne 0 ']'\n+ cd onedrive-2.5.6\n+ /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .\n+ RPM_EC=0\n++ jobs -p\n+ exit 0\nExecuting(%build): /bin/sh -e /var/tmp/rpm-tmp.b9tkxJ\n+ umask 022\n+ cd /home/alex/rpmbuild/BUILD\n+ cd onedrive-2.5.6\n+ 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'\n+ export CFLAGS\n+ 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'\n+ export CXXFLAGS\n+ 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'\n+ export FFLAGS\n+ 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'\n+ export FCFLAGS\n+ 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 '\n+ export LDFLAGS\n+ LT_SYS_LIBRARY_PATH=/usr/lib64:\n+ export LT_SYS_LIBRARY_PATH\n+ CC=gcc\n+ export CC\n+ CXX=g++\n+ export CXX\n+ '[' '-flto=auto -ffat-lto-objectsx' '!=' x ']'\n++ find . -type f -name configure -print\n+ for file in $(find . -type f -name configure -print)\n+ /usr/bin/sed -r --in-place=.backup 's/^char \\(\\*f\\) \\(\\) = /__attribute__ ((used)) char (*f) () = /g' ./configure\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ /usr/bin/sed -r --in-place=.backup 's/^char \\(\\*f\\) \\(\\);/__attribute__ ((used)) char (*f) ();/g' ./configure\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ /usr/bin/sed -r --in-place=.backup 's/^char \\$2 \\(\\);/__attribute__ ((used)) char \\$2 ();/g' ./configure\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ /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\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ /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\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ '[' 1 = 1 ']'\n+++ dirname ./configure\n++ find . -name config.guess -o -name config.sub\n+ '[' 1 = 1 ']'\n+ '[' x '!=' 'x-Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld' ']'\n++ find . -name ltmain.sh\n+ ./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\nconfigure: WARNING: unrecognized options: --disable-dependency-tracking\nchecking for a BSD-compatible install... /usr/bin/install -c\nchecking for x86_64-redhat-linux-gnu-pkg-config... /usr/bin/x86_64-redhat-linux-gnu-pkg-config\nchecking pkg-config is at least version 0.9.0... yes\nchecking for dmd... dmd\nchecking version of D compiler... 2.091.1\nchecking for curl... yes\nchecking for sqlite... yes\nchecking whether to enable dbus support... yes (on Linux)\nchecking for dbus... yes\nchecking for notify... yes\nconfigure: creating ./config.status\nconfig.status: creating Makefile\nconfig.status: creating contrib/pacman/PKGBUILD\nconfig.status: creating contrib/spec/onedrive.spec\nconfig.status: creating onedrive.1\nconfig.status: creating contrib/systemd/onedrive.service\nconfig.status: creating contrib/systemd/onedrive@.service\nconfigure: WARNING: unrecognized options: --disable-dependency-tracking\n+ make\nif [ -f .git/HEAD ] ; then \\\n        git describe --tags > version ; \\\nelse \\\n        echo v2.5.6 > version ; \\\nfi\ndmd -J. -version=NoPragma -version=NoGdk -version=Notifications -w -g -debug -gs 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 src/intune.d src/notifications/notify.d src/notifications/dnotify.d -L-lcurl -L-lsqlite3 -L-ldbus-1 -L-lnotify -L-lgdk_pixbuf-2.0 -L-lgio-2.0 -L-lgobject-2.0 -L-lglib-2.0 -L-ldl -ofonedrive\n+ RPM_EC=0\n++ jobs -p\n+ exit 0\nExecuting(%install): /bin/sh -e /var/tmp/rpm-tmp.Pwy2mS\n+ umask 022\n+ cd /home/alex/rpmbuild/BUILD\n+ '[' /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64 '!=' / ']'\n+ rm -rf /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64\n++ dirname /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64\n+ mkdir -p /home/alex/rpmbuild/BUILDROOT\n+ mkdir /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64\n+ cd onedrive-2.5.6\n+ /usr/bin/make install DESTDIR=/home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64 'INSTALL=/usr/bin/install -p' PREFIX=/home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64\nmkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/bin\n/usr/bin/install -p onedrive /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/bin/onedrive\nmkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/man/man1\n/usr/bin/install -p -m 0644 onedrive.1 /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/man/man1/onedrive.1\nmkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/etc/logrotate.d\n/usr/bin/install -p -m 0644 contrib/logrotate/onedrive.logrotate /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/etc/logrotate.d/onedrive\nmkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive\nfor file in 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; do \\\n        /usr/bin/install -p -m 0644 $file /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive; \\\ndone\nmkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/lib/systemd/user\nmkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/lib/systemd/system\n/usr/bin/install -p -m 0644 contrib/systemd/onedrive@.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/lib/systemd/system\n/usr/bin/install -p -m 0644 contrib/systemd/onedrive.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/lib/systemd/system\n+ install -D -m 0644 contrib/systemd/onedrive.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/lib/systemd/system/onedrive.service\n+ install -D -m 0644 contrib/systemd/onedrive@.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/lib/systemd/system/onedrive@.service\n+ /usr/lib/rpm/check-buildroot\n+ /usr/lib/rpm/redhat/brp-ldconfig\n+ /usr/lib/rpm/brp-compress\n+ /usr/lib/rpm/brp-strip /usr/bin/strip\n+ /usr/lib/rpm/brp-strip-comment-note /usr/bin/strip /usr/bin/objdump\n+ /usr/lib/rpm/redhat/brp-strip-lto /usr/bin/strip\n+ /usr/lib/rpm/brp-strip-static-archive /usr/bin/strip\n+ /usr/lib/rpm/redhat/brp-python-bytecompile '' 1 0\n+ /usr/lib/rpm/brp-python-hardlink\n+ /usr/lib/rpm/redhat/brp-mangle-shebangs\nProcessing files: onedrive-2.5.6-1.el9.x86_64\nExecuting(%doc): /bin/sh -e /var/tmp/rpm-tmp.2YAn9k\n+ umask 022\n+ cd /home/alex/rpmbuild/BUILD\n+ cd onedrive-2.5.6\n+ DOCDIR=/home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive\n+ export LC_ALL=C\n+ LC_ALL=C\n+ export DOCDIR\n+ /usr/bin/mkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive\n+ cp -pr readme.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive\n+ cp -pr LICENSE /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive\n+ cp -pr changelog.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive\n+ cp -pr docs/advanced-usage.md docs/application-config-options.md docs/application-security.md docs/build-rpm-howto.md docs/business-shared-items.md docs/client-architecture.md docs/contributing.md docs/docker.md docs/install.md docs/known-issues.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/webhooks.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive\n+ cp -pr config /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64/usr/share/doc/onedrive\n+ RPM_EC=0\n++ jobs -p\n+ exit 0\nProvides: config(onedrive) = 2.5.6-1.el9 onedrive = 2.5.6-1.el9 onedrive(x86-64) = 2.5.6-1.el9\nRequires(rpmlib): rpmlib(CompressedFileNames) <= 3.0.4-1 rpmlib(FileDigests) <= 4.6.0-1 rpmlib(PayloadFilesHavePrefix) <= 4.0-1\nRequires(post): systemd\nRequires(preun): systemd\nRequires(postun): systemd\nRequires: 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)(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) libdbus-1.so.3()(64bit) libdbus-1.so.3(LIBDBUS_1_3)(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)\nChecking for unpackaged file(s): /usr/lib/rpm/check-files /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.6-1.el9.x86_64\nWrote: /home/alex/rpmbuild/SRPMS/onedrive-2.5.6-1.el9.src.rpm\nWrote: /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.5.6-1.el9.x86_64.rpm\nExecuting(%clean): /bin/sh -e /var/tmp/rpm-tmp.tGKXPN\n+ umask 022\n+ cd /home/alex/rpmbuild/BUILD\n+ cd onedrive-2.5.6\n+ RPM_EC=0\n++ jobs -p\n+ exit 0\n```\n\n#### CentOS Stream release 9 RPM Package Install Process\n```text\n[alex@centos9stream ~]$ sudo yum -y install /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.5.6-1.el9.x86_64.rpm\n[sudo] password for alex: \nLast metadata expiration check: 1:21:53 ago on Tue 10 Jun 2025 06:41:27.\nDependencies resolved.\n==========================================================================================================================================================================================\n Package                                    Architecture                             Version                                         Repository                                      Size\n==========================================================================================================================================================================================\nInstalling:\n onedrive                                   x86_64                                   2.5.6-1.el9                                     @commandline                                   1.6 M\n\nTransaction Summary\n==========================================================================================================================================================================================\nInstall  1 Package\n\nTotal size: 1.6 M\nInstalled size: 8.3 M\nDownloading Packages:\nRunning transaction check\nTransaction check succeeded.\nRunning transaction test\nTransaction test succeeded.\nRunning transaction\n  Preparing        :                                                                                                                                                                  1/1 \n  Installing       : onedrive-2.5.6-1.el9.x86_64                                                                                                                                      1/1 \n  Running scriptlet: onedrive-2.5.6-1.el9.x86_64                                                                                                                                      1/1 \n  Verifying        : onedrive-2.5.6-1.el9.x86_64                                                                                                                                      1/1 \n\nInstalled:\n  onedrive-2.5.6-1.el9.x86_64                                                                                                                                                             \n\nComplete!\n[alex@centos9stream ~]$ \n[alex@centos9stream ~]$ onedrive --version\nonedrive v2.5.6\n[alex@centos9stream ~]$ onedrive --display-config\nWARNING: Configured 'threads = 8' exceeds available CPU cores (1). Capping to 'threads' to 1.\nApplication version                          = onedrive v2.5.6\nCompiled with                                = DMD 2091\nCurl version                                 = libcurl/7.76.1 OpenSSL/3.5.0 zlib/1.2.11 brotli/1.0.9 libidn2/2.3.0 libpsl/0.21.1 (+libidn2/2.3.0) libssh/0.10.4/openssl/zlib nghttp2/1.43.0\nUser Application Config path                 = /home/alex/.config/onedrive\nSystem Application Config path               = /etc/onedrive\nApplicable Application 'config' location     = /home/alex/.config/onedrive/config\nConfiguration file found in config location  = false - using application defaults\nApplicable 'sync_list' location              = /home/alex/.config/onedrive/sync_list\nApplicable 'items.sqlite3' location          = /home/alex/.config/onedrive/items.sqlite3\nConfig option 'drive_id'                     = \nConfig option 'sync_dir'                     = ~/OneDrive\nConfig option 'use_intune_sso'               = false\nConfig option 'use_device_auth'              = false\nConfig option 'enable_logging'               = false\nConfig option 'log_dir'                      = /var/log/onedrive\nConfig option 'disable_notifications'        = false\nConfig option 'skip_dir'                     = \nConfig option 'skip_dir_strict_match'        = false\nConfig option 'skip_file'                    = ~*|.~*|*.tmp|*.swp|*.partial\nConfig option 'skip_dotfiles'                = false\nConfig option 'skip_symlinks'                = false\nConfig option 'monitor_interval'             = 300\nConfig option 'monitor_log_frequency'        = 12\nConfig option 'monitor_fullscan_frequency'   = 12\nConfig option 'read_only_auth_scope'         = false\nConfig option 'dry_run'                      = false\nConfig option 'upload_only'                  = false\nConfig option 'download_only'                = false\nConfig option 'local_first'                  = false\nConfig option 'check_nosync'                 = false\nConfig option 'check_nomount'                = false\nConfig option 'resync'                       = false\nConfig option 'resync_auth'                  = false\nConfig option 'cleanup_local_files'          = false\nConfig option 'disable_permission_set'       = false\nConfig option 'transfer_order'               = default\nConfig option 'classify_as_big_delete'       = 1000\nConfig option 'disable_upload_validation'    = false\nConfig option 'disable_download_validation'  = false\nConfig option 'bypass_data_preservation'     = false\nConfig option 'no_remote_delete'             = false\nConfig option 'remove_source_files'          = false\nConfig option 'sync_dir_permissions'         = 700\nConfig option 'sync_file_permissions'        = 600\nConfig option 'space_reservation'            = 52428800\nConfig option 'permanent_delete'             = false\nConfig option 'write_xattr_data'             = false\nConfig option 'application_id'               = d50ca740-c83f-4d1b-b616-12c519384f0c\nConfig option 'azure_ad_endpoint'            = \nConfig option 'azure_tenant_id'              = \nConfig option 'user_agent'                   = ISV|abraunegg|OneDrive Client for Linux/v2.5.6\nConfig option 'force_http_11'                = false\nConfig option 'debug_https'                  = false\nConfig option 'rate_limit'                   = 0\nConfig option 'operation_timeout'            = 3600\nConfig option 'dns_timeout'                  = 60\nConfig option 'connect_timeout'              = 10\nConfig option 'data_timeout'                 = 60\nConfig option 'ip_protocol_version'          = 0\nConfig option 'threads'                      = 1\nConfig option 'max_curl_idle'                = 120\nEnvironment var 'XDG_RUNTIME_DIR'            = true\nEnvironment var 'DBUS_SESSION_BUS_ADDRESS'   = true\nConfig option 'notify_file_actions'          = false\nConfig option 'use_recycle_bin'              = false\nConfig option 'recycle_bin_path'             = /home/alex/.local/share/Trash/\n\nSelective sync 'sync_list' configured        = false\n\nConfig option 'sync_business_shared_items'   = false\n\nConfig option 'webhook_enabled'              = false\n```\n\n\n## Build RPM from SRPM using mock\n\n### Install mock on your platform\nUse the following installation instructions to install 'mock' on your platform:\n```text\nsudo yum install epel-release\nsudo yum install mock\nsudo yum install -y wget\nmkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}\n```\n\n### Configure mock\nAdd your user to the mock group:\n```text\nsudo usermod -a -G mock $USER\n```\n> [!NOTE]\n> Log out and back in for the group membership changes to take effect.\n\n### Build a Source RPM (SRPM) file\nBuild the SRPM from the provided spec file:\n```text\nwget https://github.com/abraunegg/onedrive/archive/refs/tags/v2.5.6.tar.gz -O ~/rpmbuild/SOURCES/v2.5.6.tar.gz\nwget https://raw.githubusercontent.com/abraunegg/onedrive/master/contrib/spec/onedrive.spec.in -O ~/rpmbuild/SPECS/onedrive.spec\nrpmbuild -bs ~/rpmbuild/SPECS/onedrive.spec\n```\n> [!NOTE]\n> This will build a SRPM to the following location: `/home/alex/rpmbuild/SRPMS/onedrive-2.5.6-1.el9.src.rpm` \n> \n> This SRPM will be used in the examples below:\n\n### Build Fedora 42 RPM using mock\n\n```text\n[alex@centos9stream ~]$ mock -r fedora-42-x86_64 /home/alex/rpmbuild/SRPMS/onedrive-2.5.6-1.el9.src.rpm\nINFO: mock.py version 6.2 starting (python version = 3.9.21, NVR = mock-6.2-1.el9), args: /usr/libexec/mock/mock -r fedora-42-x86_64 /home/alex/rpmbuild/SRPMS/onedrive-2.5.6-1.el9.src.rpm\nStart(bootstrap): init plugins\nINFO: selinux enabled\nFinish(bootstrap): init plugins\nStart: init plugins\nINFO: selinux enabled\nFinish: init plugins\nINFO: Signal handler active\nStart: run\nINFO: Start(/home/alex/rpmbuild/SRPMS/onedrive-2.5.6-1.el9.src.rpm)  Config(fedora-42-x86_64)\nStart: clean chroot\nFinish: clean chroot\nMock Version: 6.2\nINFO: Mock Version: 6.2\nStart(bootstrap): chroot init\nINFO: calling preinit hooks\nINFO: enabled root cache\nINFO: enabled package manager cache\nStart(bootstrap): cleaning package manager metadata\nFinish(bootstrap): cleaning package manager metadata\nINFO: Package manager dnf5 detected and used (fallback)\nFinish(bootstrap): chroot init\nStart: chroot init\nINFO: calling preinit hooks\nINFO: enabled root cache\nStart: unpacking root cache\nFinish: unpacking root cache\nINFO: enabled package manager cache\nStart: cleaning package manager metadata\nFinish: cleaning package manager metadata\nINFO: enabled HW Info plugin\nINFO: Package manager dnf5 detected and used (direct choice)\nINFO: Buildroot is handled by package management downloaded with a bootstrap image:\n  rpm-4.20.1-1.fc42.x86_64\n  rpm-sequoia-1.7.0-5.fc42.x86_64\n  dnf5-5.2.13.1-1.fc42.x86_64\n  dnf5-plugins-5.2.13.1-1.fc42.x86_64\nStart: dnf5 update\nUpdating and loading repositories:\n updates                                100% |   5.5 KiB/s |   5.6 KiB |  00m01s\n fedora                                 100% |   5.8 KiB/s |   4.2 KiB |  00m01s\nRepositories loaded.\nNothing to do.\nFinish: dnf5 update\nFinish: chroot init\nStart: build phase for onedrive-2.5.6-1.el9.src.rpm\nStart: build setup for onedrive-2.5.6-1.el9.src.rpm\nBuilding target platforms: x86_64\nBuilding for target x86_64\nsetting SOURCE_DATE_EPOCH=1749081600\nWrote: /builddir/build/SRPMS/onedrive-2.5.6-1.fc42.src.rpm\nUpdating and loading repositories:\n updates                                100% |  16.5 KiB/s |   5.6 KiB |  00m00s\n fedora                                 100% |   8.3 KiB/s |   4.2 KiB |  00m01s\nRepositories loaded.\nPackage               Arch   Version                 Repository      Size\nInstalling:\n dbus-devel           x86_64 1:1.16.0-3.fc42         fedora     131.7 KiB\n ldc                  x86_64 1:1.40.0-3.fc42         fedora      27.3 MiB\n libcurl-devel        x86_64 8.11.1-4.fc42           fedora       1.3 MiB\n sqlite-devel         x86_64 3.47.2-2.fc42           fedora     673.4 KiB\nInstalling dependencies:\n annobin-docs         noarch 12.94-1.fc42            updates     98.9 KiB\n annobin-plugin-gcc   x86_64 12.94-1.fc42            updates    993.5 KiB\n brotli               x86_64 1.1.0-6.fc42            fedora      31.6 KiB\n brotli-devel         x86_64 1.1.0-6.fc42            fedora      65.6 KiB\n cmake-filesystem     x86_64 3.31.6-2.fc42           fedora       0.0   B\n cpp                  x86_64 15.1.1-2.fc42           updates     37.9 MiB\n dbus-libs            x86_64 1:1.16.0-3.fc42         fedora     349.5 KiB\n gcc                  x86_64 15.1.1-2.fc42           updates    111.1 MiB\n gcc-plugin-annobin   x86_64 15.1.1-2.fc42           updates     57.1 KiB\n glibc-devel          x86_64 2.41-5.fc42             updates      2.3 MiB\n kernel-headers       x86_64 6.14.3-300.fc42         updates      6.5 MiB\n keyutils-libs-devel  x86_64 1.6.3-5.fc42            fedora      48.2 KiB\n krb5-devel           x86_64 1.21.3-6.fc42           updates    705.9 KiB\n ldc-libs             x86_64 1:1.40.0-3.fc42         fedora      11.6 MiB\n libcom_err-devel     x86_64 1.47.2-3.fc42           fedora      16.7 KiB\n libedit              x86_64 3.1-55.20250104cvs.fc42 fedora     244.1 KiB\n libidn2-devel        x86_64 2.3.8-1.fc42            fedora     149.1 KiB\n libkadm5             x86_64 1.21.3-6.fc42           updates    213.9 KiB\n libmpc               x86_64 1.3.1-7.fc42            fedora     164.5 KiB\n libnghttp2-devel     x86_64 1.64.0-3.fc42           fedora     295.4 KiB\n libpsl-devel         x86_64 0.21.5-5.fc42           fedora     110.3 KiB\n libselinux-devel     x86_64 3.8-2.fc42              updates    126.8 KiB\n libsepol-devel       x86_64 3.8-1.fc42              fedora     120.8 KiB\n libssh-devel         x86_64 0.11.1-4.fc42           fedora     178.0 KiB\n libverto-devel       x86_64 0.3.2-10.fc42           fedora      25.7 KiB\n libxcrypt-devel      x86_64 4.4.38-7.fc42           updates     30.8 KiB\n llvm19-filesystem    x86_64 19.1.7-13.fc42          updates      0.0   B\n llvm19-libs          x86_64 19.1.7-13.fc42          updates    124.0 MiB\n make                 x86_64 1:4.4.1-10.fc42         fedora       1.8 MiB\n openssl-devel        x86_64 1:3.2.4-3.fc42          fedora       4.3 MiB\n pcre2-devel          x86_64 10.45-1.fc42            fedora       2.1 MiB\n pcre2-utf16          x86_64 10.45-1.fc42            fedora     626.3 KiB\n pcre2-utf32          x86_64 10.45-1.fc42            fedora     598.2 KiB\n publicsuffix-list    noarch 20250116-1.fc42         fedora     329.8 KiB\n sqlite               x86_64 3.47.2-2.fc42           fedora       1.8 MiB\n systemd-devel        x86_64 257.6-1.fc42            updates    612.3 KiB\n systemd-rpm-macros   noarch 257.6-1.fc42            updates     10.7 KiB\n xml-common           noarch 0.6.3-66.fc42           fedora      78.4 KiB\n zlib-ng-compat-devel x86_64 2.2.4-3.fc42            fedora     107.0 KiB\n\nTransaction Summary:\n Installing:        43 packages\n\nTotal size of inbound packages is 103 MiB. Need to download 0 B.\nAfter this operation, 339 MiB extra will be used (install 339 MiB, remove 0 B).\n[ 1/43] ldc-1:1.40.0-3.fc42.x86_64      100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[ 2/43] dbus-devel-1:1.16.0-3.fc42.x86_ 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[ 3/43] libcurl-devel-0:8.11.1-4.fc42.x 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[ 4/43] sqlite-devel-0:3.47.2-2.fc42.x8 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[ 5/43] ldc-libs-1:1.40.0-3.fc42.x86_64 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[ 6/43] cmake-filesystem-0:3.31.6-2.fc4 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[ 7/43] dbus-libs-1:1.16.0-3.fc42.x86_6 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[ 8/43] xml-common-0:0.6.3-66.fc42.noar 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[ 9/43] sqlite-0:3.47.2-2.fc42.x86_64   100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[10/43] krb5-devel-0:1.21.3-6.fc42.x86_ 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[11/43] libkadm5-0:1.21.3-6.fc42.x86_64 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[12/43] brotli-devel-0:1.1.0-6.fc42.x86 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[13/43] brotli-0:1.1.0-6.fc42.x86_64    100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[14/43] libidn2-devel-0:2.3.8-1.fc42.x8 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[15/43] libnghttp2-devel-0:1.64.0-3.fc4 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[16/43] libpsl-devel-0:0.21.5-5.fc42.x8 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[17/43] publicsuffix-list-0:20250116-1. 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[18/43] libssh-devel-0:0.11.1-4.fc42.x8 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[19/43] openssl-devel-1:3.2.4-3.fc42.x8 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[20/43] zlib-ng-compat-devel-0:2.2.4-3. 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[21/43] gcc-0:15.1.1-2.fc42.x86_64      100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[22/43] cpp-0:15.1.1-2.fc42.x86_64      100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[23/43] libmpc-0:1.3.1-7.fc42.x86_64    100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[24/43] make-1:4.4.1-10.fc42.x86_64     100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[25/43] llvm19-libs-0:19.1.7-13.fc42.x8 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[26/43] llvm19-filesystem-0:19.1.7-13.f 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[27/43] libedit-0:3.1-55.20250104cvs.fc 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[28/43] systemd-devel-0:257.6-1.fc42.x8 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[29/43] libselinux-devel-0:3.8-2.fc42.x 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[30/43] libsepol-devel-0:3.8-1.fc42.x86 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[31/43] keyutils-libs-devel-0:1.6.3-5.f 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[32/43] libcom_err-devel-0:1.47.2-3.fc4 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[33/43] libverto-devel-0:0.3.2-10.fc42. 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[34/43] glibc-devel-0:2.41-5.fc42.x86_6 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[35/43] pcre2-devel-0:10.45-1.fc42.x86_ 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[36/43] pcre2-utf16-0:10.45-1.fc42.x86_ 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[37/43] pcre2-utf32-0:10.45-1.fc42.x86_ 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[38/43] kernel-headers-0:6.14.3-300.fc4 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[39/43] libxcrypt-devel-0:4.4.38-7.fc42 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[40/43] gcc-plugin-annobin-0:15.1.1-2.f 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[41/43] systemd-rpm-macros-0:257.6-1.fc 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[42/43] annobin-plugin-gcc-0:12.94-1.fc 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n[43/43] annobin-docs-0:12.94-1.fc42.noa 100% |   0.0   B/s |   0.0   B |  00m00s\n>>> Already downloaded                                                          \n--------------------------------------------------------------------------------\n[43/43] Total                           100% |   0.0   B/s |   0.0   B |  00m00s\nRunning transaction\n[ 1/45] Verify package files            100% |  29.0   B/s |  43.0   B |  00m01s\n[ 2/45] Prepare transaction             100% | 154.0   B/s |  43.0   B |  00m00s\n[ 3/45] Installing cmake-filesystem-0:3 100% | 583.8 KiB/s |   7.6 KiB |  00m00s\n[ 4/45] Installing libmpc-0:1.3.1-7.fc4 100% |  23.2 MiB/s | 166.1 KiB |  00m00s\n[ 5/45] Installing cpp-0:15.1.1-2.fc42. 100% | 120.6 MiB/s |  37.9 MiB |  00m00s\n[ 6/45] Installing libssh-devel-0:0.11. 100% |  19.6 MiB/s | 180.5 KiB |  00m00s\n[ 7/45] Installing zlib-ng-compat-devel 100% |  15.1 MiB/s | 108.5 KiB |  00m00s\n[ 8/45] Installing annobin-docs-0:12.94 100% |  10.9 MiB/s | 100.0 KiB |  00m00s\n[ 9/45] Installing kernel-headers-0:6.1 100% |  36.6 MiB/s |   6.7 MiB |  00m00s\n[10/45] Installing libxcrypt-devel-0:4. 100% |   2.9 MiB/s |  33.1 KiB |  00m00s\n[11/45] Installing glibc-devel-0:2.41-5 100% |  15.3 MiB/s |   2.3 MiB |  00m00s\n[12/45] Installing pcre2-utf32-0:10.45- 100% |  18.3 MiB/s | 599.1 KiB |  00m00s\n[13/45] Installing pcre2-utf16-0:10.45- 100% |  30.6 MiB/s | 627.1 KiB |  00m00s\n[14/45] Installing pcre2-devel-0:10.45- 100% |  33.8 MiB/s |   2.1 MiB |  00m00s\n[15/45] Installing libverto-devel-0:0.3 100% |   5.1 MiB/s |  26.4 KiB |  00m00s\n[16/45] Installing libcom_err-devel-0:1 100% | 761.4 KiB/s |  18.3 KiB |  00m00s\n[17/45] Installing keyutils-libs-devel- 100% |   5.4 MiB/s |  55.2 KiB |  00m00s\n[18/45] Installing libsepol-devel-0:3.8 100% |   9.6 MiB/s | 128.3 KiB |  00m00s\n[19/45] Installing libselinux-devel-0:3 100% |   4.2 MiB/s | 161.6 KiB |  00m00s\n[20/45] Installing systemd-devel-0:257. 100% |   6.2 MiB/s | 744.1 KiB |  00m00s\n[21/45] Installing libedit-0:3.1-55.202 100% |  30.0 MiB/s | 245.8 KiB |  00m00s\n[22/45] Installing llvm19-filesystem-0: 100% | 264.6 KiB/s |   1.1 KiB |  00m00s\n[23/45] Installing llvm19-libs-0:19.1.7 100% | 137.8 MiB/s | 124.0 MiB |  00m01s\n[24/45] Installing make-1:4.4.1-10.fc42 100% |  37.5 MiB/s |   1.8 MiB |  00m00s\n[25/45] Installing gcc-0:15.1.1-2.fc42. 100% | 131.7 MiB/s | 111.2 MiB |  00m01s\n[26/45] Installing openssl-devel-1:3.2. 100% |   9.0 MiB/s |   5.2 MiB |  00m01s\n[27/45] Installing publicsuffix-list-0: 100% |  53.8 MiB/s | 330.8 KiB |  00m00s\n[28/45] Installing libpsl-devel-0:0.21. 100% |  13.9 MiB/s | 113.6 KiB |  00m00s\n[29/45] Installing libnghttp2-devel-0:1 100% |  48.3 MiB/s | 296.5 KiB |  00m00s\n[30/45] Installing libidn2-devel-0:2.3. 100% |  11.8 MiB/s | 156.7 KiB |  00m00s\n[31/45] Installing brotli-0:1.1.0-6.fc4 100% |   1.3 MiB/s |  32.3 KiB |  00m00s\n[32/45] Installing brotli-devel-0:1.1.0 100% |   8.3 MiB/s |  68.0 KiB |  00m00s\n[33/45] Installing libkadm5-0:1.21.3-6. 100% |  26.4 MiB/s | 215.9 KiB |  00m00s\n[34/45] Installing krb5-devel-0:1.21.3- 100% |  18.4 MiB/s | 715.2 KiB |  00m00s\n[35/45] Installing sqlite-0:3.47.2-2.fc 100% |  41.5 MiB/s |   1.8 MiB |  00m00s\n[36/45] Installing xml-common-0:0.6.3-6 100% |   9.9 MiB/s |  81.1 KiB |  00m00s\n[37/45] Installing dbus-libs-1:1.16.0-3 100% |  42.8 MiB/s | 350.6 KiB |  00m00s\n[38/45] Installing ldc-libs-1:1.40.0-3. 100% |  85.7 MiB/s |  11.6 MiB |  00m00s\n[39/45] Installing ldc-1:1.40.0-3.fc42. 100% |  83.0 MiB/s |  27.5 MiB |  00m00s\n[40/45] Installing dbus-devel-1:1.16.0- 100% |  13.3 MiB/s | 136.5 KiB |  00m00s\n[41/45] Installing sqlite-devel-0:3.47. 100% |  54.9 MiB/s | 674.1 KiB |  00m00s\n[42/45] Installing libcurl-devel-0:8.11 100% |   3.2 MiB/s |   1.4 MiB |  00m00s\n[43/45] Installing gcc-plugin-annobin-0 100% |   1.1 MiB/s |  58.8 KiB |  00m00s\n[44/45] Installing annobin-plugin-gcc-0 100% |  14.1 MiB/s | 995.1 KiB |  00m00s\n[45/45] Installing systemd-rpm-macros-0 100% |   2.9 KiB/s |  11.3 KiB |  00m04s\nComplete!\nFinish: build setup for onedrive-2.5.6-1.el9.src.rpm\nStart: rpmbuild onedrive-2.5.6-1.el9.src.rpm\nStart: Outputting list of installed packages\nFinish: Outputting list of installed packages\nBuilding target platforms: x86_64\nBuilding for target x86_64\nsetting SOURCE_DATE_EPOCH=1749081600\nExecuting(%mkbuilddir): /bin/sh -e /var/tmp/rpm-tmp.ApSQdT\nExecuting(%prep): /bin/sh -e /var/tmp/rpm-tmp.u4DE7z\n+ umask 022\n+ cd /builddir/build/BUILD/onedrive-2.5.6-build\n+ cd /builddir/build/BUILD/onedrive-2.5.6-build\n+ rm -rf onedrive-2.5.6\n+ /usr/lib/rpm/rpmuncompress -x /builddir/build/SOURCES/v2.5.6.tar.gz\n+ STATUS=0\n+ '[' 0 -ne 0 ']'\n+ cd onedrive-2.5.6\n+ /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .\n+ RPM_EC=0\n++ jobs -p\n+ exit 0\nExecuting(%build): /bin/sh -e /var/tmp/rpm-tmp.XgQE0g\n+ umask 022\n+ cd /builddir/build/BUILD/onedrive-2.5.6-build\n+ CFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer '\n+ export CFLAGS\n+ CXXFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer '\n+ export CXXFLAGS\n+ FFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -I/usr/lib64/gfortran/modules '\n+ export FFLAGS\n+ FCFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -I/usr/lib64/gfortran/modules '\n+ export FCFLAGS\n+ VALAFLAGS=-g\n+ export VALAFLAGS\n+ RUSTFLAGS='-Copt-level=3 -Cdebuginfo=2 -Ccodegen-units=1 -Cstrip=none -Cforce-frame-pointers=yes -Clink-arg=-specs=/usr/lib/rpm/redhat/redhat-package-notes --cap-lints=warn'\n+ export RUSTFLAGS\n+ LDFLAGS='-Wl,-z,relro -Wl,--as-needed  -Wl,-z,pack-relative-relocs -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1  -Wl,--build-id=sha1 -specs=/usr/lib/rpm/redhat/redhat-package-notes '\n+ export LDFLAGS\n+ LT_SYS_LIBRARY_PATH=/usr/lib64:\n+ export LT_SYS_LIBRARY_PATH\n+ CC=gcc\n+ export CC\n+ CXX=g++\n+ export CXX\n+ cd onedrive-2.5.6\n+ CFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer '\n+ export CFLAGS\n+ CXXFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer '\n+ export CXXFLAGS\n+ FFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -I/usr/lib64/gfortran/modules '\n+ export FFLAGS\n+ FCFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -I/usr/lib64/gfortran/modules '\n+ export FCFLAGS\n+ VALAFLAGS=-g\n+ export VALAFLAGS\n+ RUSTFLAGS='-Copt-level=3 -Cdebuginfo=2 -Ccodegen-units=1 -Cstrip=none -Cforce-frame-pointers=yes -Clink-arg=-specs=/usr/lib/rpm/redhat/redhat-package-notes --cap-lints=warn'\n+ export RUSTFLAGS\n+ LDFLAGS='-Wl,-z,relro -Wl,--as-needed  -Wl,-z,pack-relative-relocs -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1  -Wl,--build-id=sha1 -specs=/usr/lib/rpm/redhat/redhat-package-notes '\n+ export LDFLAGS\n+ LT_SYS_LIBRARY_PATH=/usr/lib64:\n+ export LT_SYS_LIBRARY_PATH\n+ CC=gcc\n+ export CC\n+ CXX=g++\n+ export CXX\n+ '[' '-flto=auto -ffat-lto-objectsx' '!=' x ']'\n++ find . -type f -name configure -print\n+ for file in $(find . -type f -name configure -print)\n+ /usr/bin/sed -r --in-place=.backup 's/^char \\(\\*f\\) \\(\\) = /__attribute__ ((used)) char (*f) () = /g' ./configure\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ /usr/bin/sed -r --in-place=.backup 's/^char \\(\\*f\\) \\(\\);/__attribute__ ((used)) char (*f) ();/g' ./configure\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ /usr/bin/sed -r --in-place=.backup 's/^char \\$2 \\(\\);/__attribute__ ((used)) char \\$2 ();/g' ./configure\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ /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\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ /usr/bin/sed -r --in-place=.backup '/lt_cv_sys_global_symbol_to_cdecl=/s#(\".*\"|'\\''.*'\\'')#\"sed -n -e '\\''s/^T .* \\\\(.*\\\\)$/extern int \\\\1();/p'\\'' -e '\\''s/^$symcode* .* \\\\(.*\\\\)$/extern char \\\\1;/p'\\''\"#' ./configure\n+ diff -u ./configure.backup ./configure\n+ mv ./configure.backup ./configure\n+ '[' 1 = 1 ']'\n+++ dirname ./configure\n++ find . -name config.guess -o -name config.sub\n+ '[' 1 = 1 ']'\n+ '[' x '!=' 'x-Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld' ']'\n++ find . -name ltmain.sh\n++ grep -q runstatedir=DIR ./configure\n+ ./configure --build=x86_64-redhat-linux --host=x86_64-redhat-linux --program-prefix= --disable-dependency-tracking --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin --sbindir=/usr/bin --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\nconfigure: WARNING: unrecognized options: --disable-dependency-tracking\nchecking for a BSD-compatible install... /usr/bin/install -c\nchecking for x86_64-redhat-linux-pkg-config... no\nchecking for pkg-config... /usr/bin/pkg-config\nchecking pkg-config is at least version 0.9.0... yes\nchecking for dmd... no\nchecking for ldmd2... ldmd2\nchecking version of D compiler... 1.40.0\nchecking for curl... yes\nchecking for sqlite... yes\nchecking whether to enable dbus support... yes (on Linux)\nchecking for dbus... yes\nchecking for notify... no\nconfigure: creating ./config.status\nconfig.status: creating Makefile\nconfig.status: creating contrib/pacman/PKGBUILD\nconfig.status: creating contrib/spec/onedrive.spec\nconfig.status: creating onedrive.1\nconfig.status: creating contrib/systemd/onedrive.service\nconfig.status: creating contrib/systemd/onedrive@.service\nconfigure: WARNING: unrecognized options: --disable-dependency-tracking\n+ make\nif [ -f .git/HEAD ] ; then \\\n        git describe --tags > version ; \\\nelse \\\n        echo v2.5.6 > version ; \\\nfi\nldmd2 -J.  -w -g -debug -gs 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 src/intune.d -L-lcurl -L-lsqlite3 -L-L/usr/lib64/pkgconfig/../../lib64 -L-ldbus-1 -L-ldl -ofonedrive\n+ RPM_EC=0\n++ jobs -p\n+ exit 0\nExecuting(%install): /bin/sh -e /var/tmp/rpm-tmp.jDHAO4\n+ umask 022\n+ cd /builddir/build/BUILD/onedrive-2.5.6-build\n+ '[' /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT '!=' / ']'\n+ rm -rf /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT\n++ dirname /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT\n+ mkdir -p /builddir/build/BUILD/onedrive-2.5.6-build\n+ mkdir /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT\n+ CFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer '\n+ export CFLAGS\n+ CXXFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer '\n+ export CXXFLAGS\n+ FFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -I/usr/lib64/gfortran/modules '\n+ export FFLAGS\n+ FCFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 -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 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -I/usr/lib64/gfortran/modules '\n+ export FCFLAGS\n+ VALAFLAGS=-g\n+ export VALAFLAGS\n+ RUSTFLAGS='-Copt-level=3 -Cdebuginfo=2 -Ccodegen-units=1 -Cstrip=none -Cforce-frame-pointers=yes -Clink-arg=-specs=/usr/lib/rpm/redhat/redhat-package-notes --cap-lints=warn'\n+ export RUSTFLAGS\n+ LDFLAGS='-Wl,-z,relro -Wl,--as-needed  -Wl,-z,pack-relative-relocs -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1  -Wl,--build-id=sha1 -specs=/usr/lib/rpm/redhat/redhat-package-notes '\n+ export LDFLAGS\n+ LT_SYS_LIBRARY_PATH=/usr/lib64:\n+ export LT_SYS_LIBRARY_PATH\n+ CC=gcc\n+ export CC\n+ CXX=g++\n+ export CXX\n+ cd onedrive-2.5.6\n+ /usr/bin/make install DESTDIR=/builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT 'INSTALL=/usr/bin/install -p' PREFIX=/builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT\nmkdir -p /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/bin\n/usr/bin/install -p onedrive /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/bin/onedrive\nmkdir -p /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/man/man1\n/usr/bin/install -p -m 0644 onedrive.1 /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/man/man1/onedrive.1\nmkdir -p /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/etc/logrotate.d\n/usr/bin/install -p -m 0644 contrib/logrotate/onedrive.logrotate /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/etc/logrotate.d/onedrive\nmkdir -p /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\nfor file in 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; do \\\n        /usr/bin/install -p -m 0644 $file /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive; \\\ndone\n+ install -D -m 0644 contrib/systemd/onedrive@.service /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/lib/systemd/system/onedrive@.service\n+ install -D -m 0644 contrib/systemd/onedrive.service /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/lib/systemd/user/onedrive.service\n+ /usr/lib/rpm/check-buildroot\n+ /usr/lib/rpm/redhat/brp-ldconfig\n+ /usr/lib/rpm/brp-compress\n+ /usr/lib/rpm/brp-strip /usr/bin/strip\n+ /usr/lib/rpm/brp-strip-comment-note /usr/bin/strip /usr/bin/objdump\n+ /usr/lib/rpm/redhat/brp-strip-lto /usr/bin/strip\n+ /usr/lib/rpm/brp-strip-static-archive /usr/bin/strip\n+ /usr/lib/rpm/check-rpaths\n+ /usr/lib/rpm/redhat/brp-mangle-shebangs\n+ /usr/lib/rpm/brp-remove-la-files\n+ env /usr/lib/rpm/redhat/brp-python-bytecompile '' 1 0 -j1\n+ /usr/lib/rpm/redhat/brp-python-hardlink\n+ /usr/bin/add-determinism --brp -j1 /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT\nScanned 14 directories and 26 files,\n               processed 1 inodes,\n               0 modified (0 replaced + 0 rewritten),\n               0 unsupported format, 0 errors\nReading /builddir/build/BUILD/onedrive-2.5.6-build/SPECPARTS/rpm-debuginfo.specpart\nProcessing files: onedrive-2.5.6-1.fc42.x86_64\nExecuting(%doc): /bin/sh -e /var/tmp/rpm-tmp.2lS8Ty\n+ umask 022\n+ cd /builddir/build/BUILD/onedrive-2.5.6-build\n+ cd onedrive-2.5.6\n+ DOCDIR=/builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ export LC_ALL=C.UTF-8\n+ LC_ALL=C.UTF-8\n+ export DOCDIR\n+ /usr/bin/mkdir -p /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/readme.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/LICENSE /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/changelog.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/advanced-usage.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/application-config-options.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/application-security.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/build-rpm-howto.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/business-shared-items.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/client-architecture.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/contributing.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/docker.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/install.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/known-issues.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/national-cloud-deployments.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/podman.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/privacy-policy.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/sharepoint-libraries.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/terms-of-service.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/ubuntu-package-install.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/usage.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/docs/webhooks.md /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ cp -pr /builddir/build/BUILD/onedrive-2.5.6-build/onedrive-2.5.6/config /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT/usr/share/doc/onedrive\n+ RPM_EC=0\n++ jobs -p\n+ exit 0\nProvides: config(onedrive) = 2.5.6-1.fc42 onedrive = 2.5.6-1.fc42 onedrive(x86-64) = 2.5.6-1.fc42\nRequires(rpmlib): rpmlib(CompressedFileNames) <= 3.0.4-1 rpmlib(FileDigests) <= 4.6.0-1 rpmlib(PayloadFilesHavePrefix) <= 4.0-1\nRequires(post): systemd\nRequires(preun): systemd\nRequires(postun): systemd\nRequires: 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)(64bit) libc.so.6(GLIBC_2.3.2)(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.7)(64bit) libc.so.6(GLIBC_2.8)(64bit) libcurl.so.4()(64bit) libdbus-1.so.3()(64bit) libdbus-1.so.3(LIBDBUS_1_3)(64bit) libdruntime-ldc-shared.so.110()(64bit) libgcc_s.so.1()(64bit) libgcc_s.so.1(GCC_3.0)(64bit) libm.so.6()(64bit) libm.so.6(GLIBC_2.2.5)(64bit) libphobos2-ldc-shared.so.110()(64bit) libsqlite3.so.0()(64bit) rtld(GNU_HASH)\nChecking for unpackaged file(s): /usr/lib/rpm/check-files /builddir/build/BUILD/onedrive-2.5.6-build/BUILDROOT\nWrote: /builddir/build/RPMS/onedrive-2.5.6-1.fc42.x86_64.rpm\nFinish: rpmbuild onedrive-2.5.6-1.el9.src.rpm\nFinish: build phase for onedrive-2.5.6-1.el9.src.rpm\nINFO: Done(/home/alex/rpmbuild/SRPMS/onedrive-2.5.6-1.el9.src.rpm) Config(fedora-42-x86_64) 0 minutes 54 seconds\nINFO: Results and/or logs in: /var/lib/mock/fedora-42-x86_64/result\nFinish: run\n```\n\n\n\n\n"
  },
  {
    "path": "docs/business-shared-items.md",
    "content": "# How to sync OneDrive Business Shared Items\n\n> [!CAUTION]\n> 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.\n\n> [!CAUTION]\n> 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.\n> \n> 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:\n> * Removing `sync_business_shared_folders = \"true|false\"` from your 'config' file\n> * Removing the 'business_shared_folders' file \n> * Removing any local data | shared folder data from your configured 'sync_dir' to ensure that there are no conflicts or issues.\n> * Removing any configuration online that might be related to using this feature prior to v2.5.0\n\n## Process Overview\nSyncing OneDrive Business Shared Folders requires additional configuration for your 'onedrive' client:\n1.  From the OneDrive web interface, review the 'Shared' objects that have been shared with you.\n2.  Select the applicable folder, and click the 'Add shortcut to My files', which will then add this to your 'My files' folder\n3.  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.\n4.  Test the configuration using '--dry-run'\n5.  Remove the use of '--dry-run' and sync the OneDrive Business Shared folders as required\n\n### Enable syncing of OneDrive Business Shared Items via config file\n```text\nsync_business_shared_items = \"true\"\n```\n\n### Disable syncing of OneDrive Business Shared Items via config file\n```text\nsync_business_shared_items = \"false\"\n```\n\n## Syncing OneDrive Business Shared Folders\nUse the following steps to add a OneDrive Business Shared Folder to your account:\n1. Login to Microsoft OneDrive online, and navigate to 'Shared' from the left hand side pane\n\n![objects_shared_with_me](./images/objects_shared_with_me.png)\n\n2. Select the respective folder you wish to sync, and click the 'Add shortcut to My files' at the top of the page\n\n![add_shared_folder](./images/add_shared_folder.png)\n\n3. The final result online will look like this:\n\n![shared_folder_added](./images/shared_folder_added.png)\n\nWhen using Microsoft Windows, this shared folder will appear as the following:\n\n![windows_view_shared_folders](./images/windows_view_shared_folders.png)\n\n4. 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:\n\n```\n...\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 4\nFinished processing /delta JSON response from the OneDrive API\nProcessing 3 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\nCreating local directory: ./my_shared_folder\nQuota information is restricted or not available for this drive.\nSyncing this OneDrive Business Shared Folder: my_shared_folder\nFetching /delta response from the OneDrive API for Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 6\nFinished processing /delta JSON response from the OneDrive API\nProcessing 6 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\nCreating local directory: ./my_shared_folder/asdf\nCreating local directory: ./my_shared_folder/original_data\nNumber of items to download from OneDrive: 3\nDownloading file: my_shared_folder/my_folder/file_one.txt ... done\nDownloading file: my_shared_folder/my_folder/file_two.txt ... done\nDownloading file: my_shared_folder/original_data/file1.data ... done\nPerforming a database consistency and integrity check on locally stored data\n...\n```\n\nWhen this is viewed locally, on Linux, this shared folder is seen as the following:\n\n![linux_shared_folder_view](./images/linux_shared_folder_view.png)\n\nAny shared folder you add can utilise any 'client side filtering' rules that you have created.\n\n\n## Syncing OneDrive Business Shared Files\nThere are two methods to support the syncing OneDrive Business Shared Files with the OneDrive Application\n1. 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.\n2. 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.\n\n### Syncing OneDrive Business Shared Files using Option 1\n1. As per the above method for adding folders, select the shared file, then select to 'Add shortcut' to the file\n\n![add_shared_file_shortcut](./images/add_shared_file_shortcut.png)\n\n2. The final result online will look like this:\n\n![add_shared_file_shortcut_added](./images/online_shared_file_link.png)\n\nWhen using Microsoft Windows, this shared file will appear as the following:\n\n![windows_view_shared_file_link](./images/windows_view_shared_file_link.png)\n\n3. 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.\n```\n...\nAll application operations will be performed in the configured local 'sync_dir' directory: /home/alex/OneDrive\nFetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 2\nFinished processing /delta JSON response from the OneDrive API\nProcessing 1 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\nNumber of items to download from OneDrive: 1\nDownloading file: ./file to share.docx.url ... done\nSyncing this OneDrive Business Shared Folder: my_shared_folder\nFetching /delta response from the OneDrive API for Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 0\nFinished processing /delta JSON response from the OneDrive API\nNo additional changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive\nQuota information is restricted or not available for this drive.\nPerforming a database consistency and integrity check on locally stored data\nProcessing DB entries for this Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT\nQuota information is restricted or not available for this drive.\n...\n```\n\nWhen this is viewed locally, on Linux, this shared folder is seen as the following:\n\n![linux_view_shared_file_link](./images/linux_view_shared_file_link.png)\n\nAny shared file link you add can utilise any 'client side filtering' rules that you have created.\n\n\n### Syncing OneDrive Business Shared Files using Option 2\n\n> [!IMPORTANT]\n> 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.\n\n1. Review all items that have been shared with you by using `onedrive --list-shared-items`. This should display output similar to the following:\n```\n...\nListing available OneDrive Business Shared Items:\n\n-----------------------------------------------------------------------------------\nShared File:     large_document_shared.docx\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared File:     no_download_access.docx\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared File:     online_access_only.txt\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared File:     read_only.txt\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared File:     qewrqwerwqer.txt\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared File:     dummy_file_to_share.docx\nShared By:       testuser2 testuser2 (testuser2@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared Folder:   Sub Folder 2\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared File:     file to share.docx\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared Folder:   Top Folder\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared Folder:   my_shared_folder\nShared By:       testuser2 testuser2 (testuser2@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\nShared Folder:   Jenkins\nShared By:       test user (testuser@mynasau3.onmicrosoft.com)\n-----------------------------------------------------------------------------------\n...\n```\n\n2. If applicable, add entries to a 'sync_list' file, to only sync the shared files that are of importance to you.\n\n3. 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:\n\n```\n...\nFinished processing /delta JSON response from the OneDrive API\nNo additional changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive\nSyncing this OneDrive Business Shared Folder: my_shared_folder\nFetching /delta response from the OneDrive API for Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 0\nFinished processing /delta JSON response from the OneDrive API\nNo additional changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive\nQuota information is restricted or not available for this drive.\nCreating the OneDrive Business Shared Files Local Directory: /home/alex/OneDrive/Files Shared With Me\nChecking for any applicable OneDrive Business Shared Files which need to be synced locally\nCreating the OneDrive Business Shared File Users Local Directory: /home/alex/OneDrive/Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)\nCreating the OneDrive Business Shared File Users Local Directory: /home/alex/OneDrive/Files Shared With Me/testuser2 testuser2 (testuser2@mynasau3.onmicrosoft.com)\nNumber of items to download from OneDrive: 7\nDownloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/file to share.docx ... done\nOneDrive returned a 'HTTP 403 - Forbidden' - gracefully handling error\nUnable 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\nERROR: File failed to download. Increase logging verbosity to determine why.\nDownloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/no_download_access.docx ... failed!\nDownloading file: Files Shared With Me/testuser2 testuser2 (testuser2@mynasau3.onmicrosoft.com)/dummy_file_to_share.docx ... done\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 0%   |  ETA    --:--:--\nDownloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/online_access_only.txt ... done\nDownloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/read_only.txt ... done\nDownloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/qewrqwerwqer.txt ... done\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 5%   |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 10%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 15%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 20%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 25%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 30%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 35%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 40%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 45%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 50%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 55%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 60%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 65%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 70%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 75%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 80%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 85%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 90%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 95%  |  ETA    00:00:00\nDownloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 100% | DONE in 00:00:00\nQuota information is restricted or not available for this drive.\nDownloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... done\nQuota information is restricted or not available for this drive.\nQuota information is restricted or not available for this drive.\nPerforming a database consistency and integrity check on locally stored data\nProcessing DB entries for this Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT\nQuota information is restricted or not available for this drive.\n...\n```\n\nWhen this is viewed locally, on Linux, this 'Files Shared With Me' and content is seen as the following:\n\n![files_shared_with_me_folder](./images/files_shared_with_me_folder.png)\n\nUnfortunately there is no Microsoft Windows equivalent for this capability.\n\n## Known Issues\nShared 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.\n\nShared folders that match this scenario, when you view 'Shared' via OneDrive online, will have a 'world' symbol as per below:\n\n![shared_with_me](./images/shared_with_me.JPG)\n\nThis issue is being tracked by: [#966](https://github.com/abraunegg/onedrive/issues/966)\n"
  },
  {
    "path": "docs/client-architecture.md",
    "content": "# OneDrive Client for Linux Application Architecture\n\n## How does the client work at a high level?\nThe 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:\n\n![client_use_of_libcurl](./puml/client_use_of_libcurl.png)\n\nDepending 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:\n\n*  Downgrade all HTTPS operations to use HTTP1.1 (Config Option: `force_http_11`)\n*  Control how long a specific transfer should take before it is considered too slow and aborted (Config Option: `operation_timeout`)\n*  Control libcurl handling of DNS Cache Timeout (Config Option: `dns_timeout`)\n*  Control the maximum time allowed for the connection to be established (Config Option: `connect_timeout`)\n*  Control the timeout for activity on an established HTTPS connection (Config Option: `data_timeout`)\n*  Control what IP protocol version should be used when communicating with OneDrive (Config Option: `ip_protocol_version`)\n*  Control what User Agent is presented to Microsoft services (Config Option: `user_agent`)\n\n> [!IMPORTANT]\n> 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\n\nDiving 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.\n\n![High Level Application Sequence](./puml/high_level_operational_process.png)\n\nThe application operational processes have several high level key stages:\n\n1. **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.\n\n2. **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.\n\n3. **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.\n\n4. **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.\n\n5. **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.\n\n6. **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.\n\n## What are the operational modes of the client?\n\nThere are 2 main operational modes that the client can utilise:\n\n1. Standalone sync mode that performs a single sync action against Microsoft OneDrive. This method is used when you utilise `--sync`.\n2. 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`.\n\nBy default, both sync modes (`--sync` and `--monitor`)treat the data stored online in Microsoft OneDrive as the 'source-of-truth'. This means the client will first examine your OneDrive account for any changes (additions, modifications, deletions) and apply those changes to your local file system. After this, any local changes are uploaded, and finally, a second check ensures your local state matches the online state. This mirrors the behaviour of the Microsoft OneDrive Client for Windows.\n\n![Default Sync Flow Process](./puml/default_sync_flow.png)\n\nWhen using the client with the `--local-first` option, the sync flow is reversed. The client treats your local files as the 'source-of-truth'. Local changes are processed first and pushed to Microsoft OneDrive online. Only after local changes have been uploaded will the client check for any remote changes (this includes online additions, modifications and deletions) and apply those to your local system as needed, ensuring the final local state is consistent with that what is now online.\n\n![Local First Sync Flow Process](./puml/local_first_sync_process.png)\n\n> [!IMPORTANT]\n> When using `--sync --local-first`, a locally deleted file will only be deleted online if it was already in sync with its online counterpart.\n> * If the file was never synced, the client cannot know that the corresponding online file should be removed. In this case, the online file may be downloaded again\n> * Using `--resync` makes this behaviour more likely because it wipes all local knowledge of what was previously synced, so local deletions will not be recognised\n>\n> When using `--monitor --local-first`, file system watches (via inotify) will detect local deletions. This event will automatically trigger removal of the online file, and if exists and matches the local data, the file online will be removed.\n\n> [!IMPORTANT]\n> 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.\n\n## OneDrive Client for Linux High Level Activity Flows\n\nThe diagrams below show the high level process flow and decision making when running the application\n\n### Main functional activity flows\n![Main Activity](./puml/main_activity_flows.png)\n\n### Processing a potentially new local item\n![applyPotentiallyNewLocalItem](./puml/applyPotentiallyNewLocalItem.png)\n\n### Processing a potentially changed local item\n![applyPotentiallyChangedItem](./puml/applyPotentiallyChangedItem.png)\n\n### Download a file from Microsoft OneDrive\n![downloadFile](./puml/downloadFile.png)\n\n### Upload a modified file to Microsoft OneDrive\n![uploadModifiedFile](./puml/uploadModifiedFile.png)\n\n### Upload a new local file to Microsoft OneDrive\n![uploadFile](./puml/uploadFile.png)\n\n### Determining if an 'item' is synchronised between Microsoft OneDrive and the local file system\n![Item Sync Determination](./puml/is_item_in_sync.png)\n\n### Determining if an 'item' is excluded due to 'Client Side Filtering' rules\n\nBy default, the OneDrive Client for Linux will sync all files and folders between Microsoft OneDrive and the local filesystem.\n\nClient 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:\n\n* **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.\n\n* **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.\n\n* **skip_file:** Specific files can be excluded from synchronisation using this option. It provides flexibility in selecting which files are essential for cloud storage.\n\n* **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.\n\nThis exclusion process can be illustrated by the following activity diagram. A 'true' return value means that the path being evaluated needs to be excluded:\n\n![Client Side Filtering Determination](./puml/client_side_filtering_rules.png)\n\n## Understanding how the client processes online state\nWhen you see `Fetching items from the OneDrive API for Drive ID:` or `Generating a /delta response from the OneDrive API for this Drive ID:` the client isn’t stuck—it’s working through paged change sets from Microsoft Graph using your current delta token, reconciling them with the local database, and safely scheduling work. Microsoft Graph returns paged results and signals either `@odata.nextLink` (more pages to fetch) or `@odata.deltaLink` (caught up; keep this token for next time) - the client follows those links until it reaches a stable point. Page sizing and paging behaviour are controlled by the Microsoft Graph API service.\n\n### What a typical cycle looks like\n1. **Fetching online state**\n    * **Application Output:** `Fetching items from the OneDrive API for Drive ID: …` or `Generating a /delta response from the OneDrive API for this Drive ID:`\n    * The client requests the next page of changes using your current delta token.\n2. **Processing received items**\n    * **Application Output:** `Processing N applicable changes and items received from Microsoft OneDrive`\n    * Each item received is classified (add/update/delete/excluded), matched against local state, and queued for action.\n3. **Execute required actions**\n    * Download new or modified files, Delete local data that has been deleted online, Create new local directories\n4. **Database Integrity**\n    * **Application Output:** `Performing a database consistency and integrity check on locally stored data`\n    * Integrity pass to prevent state corruption\n5. **Local scan for new local data**\n    * **Application Output:** `Scanning the local file system '…' for new data to upload`\n    * Traverse local filesystem, honouring client side filtering rules\n6. **True-Up**\n    * **Application Output:** `Performing a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process`\n    * Final scan of online to ensure that everything is in the state it is meant to be\n\n### Why first runs or --resync take longer\nA first run (or a deliberate `--resync`) must enumerate the entire tree to establish a known-good baseline; subsequent incremental runs are much faster because the delta token limits work to just the changes since last time.\n\n### What affects performance the most\n* **Item count & Online structure:** Many folders and files dominate metadata work leading to more metadata churn\n* **Network** Latency and throughput directly affect how quickly we can iterate Microsoft Graph API responses and transfer content.\n* **Local Disk & filesystem:** SSDs perform metadata and DB work far faster than spinning disks or remote mounts. Your filesystem type (e.g., ext4, XFS, ZFS) matters and should be tuned appropriately.\n* **File Indexing:** Disable File Indexing (Tracker, Baloo, Searchmonkey, Pinot and others) as these are adding latency and disk I/O to your operations slowing down your performance.\n* **CPU & memory:** Classification and hashing are CPU-bound; insufficient RAM or swap can slow DB and traversal work.\n\n## Delta Response vs Generated Delta Response\nBy default, the client uses Microsoft Graph’s `/delta` to retrieve changes efficiently. In a few situations, however, using `/delta` would be wrong or unsafe for your intent. In those cases the client generates a delta by walking the relevant online subtree and synthesising the current state before reconciling it locally. This is intentionally slower but correct.\n\n### When the client deliberately generates a delta\n* Some national cloud deployments where a needed delta endpoint/feature isn’t available. Capabilities differ by resource and cloud; when a required delta isn’t available, we walk the tree and synthesise the change set.\n* The use of `--single-directory` scope. A naïve drive-level /delta can include changes outside your intended scope. Generating a delta ensures only the in-scope subtree is considered.\n* The use of `--download-only --cleanup-local-files`. Raw /delta may replay online delete/replace churn that would remove valid local files you intend to keep. Generated delta captures the current online state and intentionally ignores those intermediate events to protect local data.\n* The use of 'Shared Folders'. Calling `/delta` on a shared path can be rooted at the owner’s drive, so your filters may not match what you see as “the shared folder”. Generated delta walks the shared subtree and normalises paths so the queue reflects what’s truly shared with you.\n\n## File conflict handling - default operational modes\n\nWhen 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.\n\nWhen 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.\n\nDue 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.\n\n> [!IMPORTANT]\n> 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:\n>\n>    **filename-hostname-safeBackup-number.file_extension**\n> \n> For example:\n> ```\n> -rw-------.  1 alex alex 53402 Sep 21 08:25 file5.data\n> -rw-------.  1 alex alex 53423 Nov 13 18:18 file5-onedrive-client-dev-safeBackup-0001.data\n> -rw-------.  1 alex alex 53422 Nov 13 18:19 file5-onedrive-client-dev-safeBackup-0002.data\n> ```\n>\n> In client versions v2.5.2 and below, the renamed file have the following naming convention:\n>\n>    **filename-hostname-number.file_extension**\n> \n> resulting in backup filenames of the following format:\n> ```\n> -rw-------.  1 alex alex 53402 Sep 21 08:25 file5.data\n> -rw-------.  1 alex alex 53432 Nov 14 05:22 file5-onedrive-client-dev-2.data\n> -rw-------.  1 alex alex 53435 Nov 14 05:24 file5-onedrive-client-dev-3.data\n> -rw-------.  1 alex alex 53419 Nov 14 05:22 file5-onedrive-client-dev.data\n> ```\n>\n\n> [!CAUTION]\n> The creation of backup files when there is a conflict to avoid local data loss can be disabled.\n> \n> To do this, utilise the configuration option **'bypass_data_preservation'** \n> ```\n> bypass_data_preservation = \"true\"\n> ```\n> \n> If enable this option, you may experience data loss on your local data as the existing local file will be over-written with data from OneDrive online. Use with extreme care and caution.\n\n> [!TIP]\n> If you wish to avoid having these backup files from being uploaded to your online OneDrive account, you can utilise the configuration option **'skip_file'** to skip these files from being uploaded.\n>\n> For example:\n> ```\n> skip_file = \"~*|.~*|*.tmp|*.swp|*.partial|*-safeBackup-*\"\n> ```\n> This example retails the application defaults for 'skip_file' and adds an entry to skip any 'safeBackup' generated file.\n\n### Default Operational Modes - Conflict Handling\n\n#### Scenario\n1. Create a local file\n2. Perform a sync with Microsoft OneDrive using `onedrive --sync`\n3. Modify file online\n4. Modify file locally with different data|contents\n5. Perform a sync with Microsoft OneDrive using `onedrive --sync`\n\n![conflict_handling_default](./puml/conflict_handling_default.png)\n\n#### Evidence of Conflict Handling\n```\n...\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 2\nFinished processing /delta JSON response from the OneDrive API\nProcessing 1 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\nNumber of items to download from OneDrive: 1\nThe local file to replace (./1.txt) has been modified locally since the last download. Renaming it to avoid potential local data loss.\nThe 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\nDownloading file ./1.txt ... done\nPerforming a database consistency and integrity check on locally stored data\nProcessing DB entries for this Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing ~/OneDrive\nThe directory has not changed\nProcessing α\n...\nThe file has not changed\nProcessing เอกสาร\nThe directory has not changed\nProcessing 1.txt\nThe file has not changed\nScanning the local file system '~/OneDrive' for new data to upload\n...\nNew items to upload to OneDrive: 1\nTotal New Data to Upload:        52 Bytes\nUploading new file ./1-onedrive-client-dev.txt ... done.\nPerforming a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process\nFetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 2\nFinished processing /delta JSON response from the OneDrive API\nProcessing 1 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\n\nSync with Microsoft OneDrive is complete\nWaiting for all internal threads to complete before exiting application\n```\n\n### Default Operational Modes - Conflict Handling with --resync\n\n#### Scenario\n1. Create a local file\n2. Perform a sync with Microsoft OneDrive using `onedrive --sync`\n3. Modify file online\n4. Modify file locally with different data|contents\n5. Perform a sync with Microsoft OneDrive using `onedrive --sync --resync`\n\n![conflict_handling_default_resync](./puml/conflict_handling_default_resync.png)\n\n#### Evidence of Conflict Handling\n```\n...\nDeleting the saved application sync status ...\nUsing IPv4 and IPv6 (if configured) for all network operations\nChecking Application Version ...\n...\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 14\nFinished processing /delta JSON response from the OneDrive API\nProcessing 13 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\nLocal file time discrepancy detected: ./1.txt\nThis 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)\nThe local file has a different hash when compared to remote file hash\nLocal item does not exist in local database - replacing with file from OneDrive - failed download?\nThe 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\nNumber of items to download from OneDrive: 1\nDownloading file ./1.txt ... done\nPerforming a database consistency and integrity check on locally stored data\nProcessing DB entries for this Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing ~/OneDrive\nThe directory has not changed\nProcessing α\n...\nProcessing เอกสาร\nThe directory has not changed\nProcessing 1.txt\nThe file has not changed\nScanning the local file system '~/OneDrive' for new data to upload\n...\nNew items to upload to OneDrive: 1\nTotal New Data to Upload:        52 Bytes\nUploading new file ./1-onedrive-client-dev.txt ... done.\nPerforming a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process\nFetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 2\nFinished processing /delta JSON response from the OneDrive API\nProcessing 1 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\n\nSync with Microsoft OneDrive is complete\nWaiting for all internal threads to complete before exiting application\n```\n\n## File conflict handling - local-first operational mode\n\nWhen 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.\n\nHowever - 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.\n\nAdditionally, 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.\n\nDue 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`.\n\n### Local First Operational Modes - Conflict Handling\n\n#### Scenario\n1. Create a local file\n2. Perform a sync with Microsoft OneDrive using `onedrive --sync --local-first`\n3. Modify file locally with different data|contents\n4. Modify file online with different data|contents\n5. Perform a sync with Microsoft OneDrive using `onedrive --sync --local-first`\n\n![conflict_handling_local-first_default](./puml/conflict_handling_local-first_default.png)\n\n#### Evidence of Conflict Handling\n```\nReading configuration file: /home/alex/.config/onedrive/config\n...\nUsing IPv4 and IPv6 (if configured) for all network operations\nChecking Application Version ...\n...\nSync Engine Initialised with new Onedrive API instance\nAll application operations will be performed in the configured local 'sync_dir' directory: /home/alex/OneDrive\nPerforming a database consistency and integrity check on locally stored data\nProcessing DB entries for this Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing ~/OneDrive\nThe directory has not changed\nProcessing α\nThe directory has not changed\n...\nThe file has not changed\nProcessing เอกสาร\nThe directory has not changed\nProcessing 1.txt\nLocal file time discrepancy detected: 1.txt\nThe file content has changed locally and has a newer timestamp, thus needs to be uploaded to OneDrive\nChanged local items to upload to OneDrive: 1\nThe 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\nUploading new file 1-onedrive-client-dev.txt ... done.\nScanning the local file system '~/OneDrive' for new data to upload\n...\nFetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 3\nFinished processing /delta JSON response from the OneDrive API\nProcessing 2 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\nNumber of items to download from OneDrive: 1\nDownloading file ./1.txt ... done\n\nSync with Microsoft OneDrive is complete\nWaiting for all internal threads to complete before exiting application\n```\n\n\n### Local First Operational Modes - Conflict Handling with --resync\n\n#### Scenario\n1. Create a local file\n2. Perform a sync with Microsoft OneDrive using `onedrive --sync --local-first`\n3. Modify file locally with different data|contents\n4. Modify file online with different data|contents\n5. Perform a sync with Microsoft OneDrive using `onedrive --sync --local-first --resync`\n\n![conflict_handling_local-first_resync](./puml/conflict_handling_local-first_resync.png)\n\n#### Evidence of Conflict Handling\n```\n...\nAre you sure you wish to proceed with --resync? [Y/N] y\n\nDeleting the saved application sync status ...\nUsing IPv4 and IPv6 (if configured) for all network operations\n...\nSync Engine Initialised with new Onedrive API instance\nAll application operations will be performed in the configured local 'sync_dir' directory: /home/alex/OneDrive\nPerforming a database consistency and integrity check on locally stored data\nProcessing DB entries for this Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing ~/OneDrive\nThe directory has not changed\nScanning the local file system '~/OneDrive' for new data to upload\nSkipping item - excluded by sync_list config: ./random_25k_files\nOneDrive Client requested to create this directory online: ./α\nThe requested directory to create was found on OneDrive - skipping creating the directory: ./α\n...\nNew items to upload to OneDrive: 9\nTotal New Data to Upload:        49 KB\n...\nThe file we are attempting to upload as a new file already exists on Microsoft OneDrive: ./1.txt\nSkipping uploading this item as a new file, will upload as a modified file (online file already exists): ./1.txt\nThe 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\nUploading new file ./1-onedrive-client-dev.txt ... done.\nFetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA\nProcessing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 15\nFinished processing /delta JSON response from the OneDrive API\nProcessing 14 applicable changes and items received from Microsoft OneDrive\nProcessing OneDrive JSON item batch [1/1] to ensure consistent local state\nNumber of items to download from OneDrive: 1\nDownloading file ./1.txt ... done\n\nSync with Microsoft OneDrive is complete\nWaiting for all internal threads to complete before exiting application\n```\n\n## Client Functional Component Architecture Relationships\n\nThe diagram below shows the main functional relationship of application code components, and how these relate to each relevant code module within this application:\n\n![Functional Code Components](./puml/code_functional_component_relationships.png)\n\n## Database Schema\n\nThe diagram below shows the database schema that is used within the application\n\n![Database Schema](./puml/database_schema.png)\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "# OneDrive Client for Linux: Coding Style Guidelines\n\n## Introduction\n\nThis document outlines the coding style guidelines for code contributions for the OneDrive Client for Linux. \n\nThese guidelines are intended to ensure the codebase remains clean, well-organised, and accessible to all contributors, new and experienced alike.\n\n## Code Layout\n> [!NOTE]\n> When developing any code contribution, please utilise either Microsoft Visual Studio Code or Notepad++.\n\n### Indentation\nMost of the codebase utilises tabs for space indentation, with 4 spaces to a tab. Please keep to this convention.\n\n### Line Length\nTry 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.\n\nIf you wish to use shorter line lengths (80 characters for example), please do not follow this sort of example:\n```code\n...\n\tvoid functionName(\n\t\tstring somevar,\n\t\tbool someOtherVar,\n\t\tcost(char) anotherVar=null\n\t){\n....\n```\n\n### Coding Style | Braces\nPlease 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.\n\nWhen 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.\n\n```code\n\t// What this if statement is doing\n\tif (condition) {\n\t\t// The condition was true\n\t\t.....\n\t} else {\n\t\t// The condition was false\n\t\t.....\n\t}\n\n\t// Loop 10 times to do something\n\tfor (int i = 0; i < 10; i++) {\n\t\t// Loop body\n\t}\n\n\t// This function is to do this\n\tvoid functionExample() {\n\t\t// Function body\n\t}\n```\n\n## Naming Conventions\n\n### Variables and Functions\nPlease use `camelCase` for variable and function names.\n\n### Classes and Interfaces\nPlease use `PascalCase` for classes, interfaces, and structs.\n\n### Constants\nUse uppercase with underscores between words.\n\n## Documentation\n\n### Language and Spelling\nTo 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.\n\nFor 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.\n\n### Code Comments\nPlease 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.\n\nIf fixing a 'bug', please add a link to the GitHub issue being addressed as a comment, for example:\n```code\n...\n\t// Before discarding change - does this ID still exist on OneDrive - as in IS this \n\t// potentially a --single-directory sync and the user 'moved' the file out of the 'sync-dir' to another OneDrive folder\n\t// This is a corner edge case - https://github.com/skilion/onedrive/issues/341\n\n\t// What is the original local path for this ID in the database? Does it match 'syncFolderChildPath'\n\tif (itemdb.idInLocalDatabase(driveId, item[\"id\"].str)){\n\t\t// item is in the database\n\t\tstring originalLocalPath = computeItemPath(driveId, item[\"id\"].str);\n...\n```\n\nAll code should be clearly commented.\n\n### Application Logging Output\nIf making changes to any application logging output, please first discuss this either via direct communication or email.\n\nFor reference, below are the available application logging output functions and examples:\n```code\n\n\t// most used\n\taddLogEntry(\"Basic 'info' message\", [\"info\"]); .... or just use addLogEntry(\"Basic 'info' message\");\n\taddLogEntry(\"Basic 'verbose' message\", [\"verbose\"]);\n\taddLogEntry(\"Basic 'debug' message\", [\"debug\"]);\n\t\n\t// GUI notify only\n\taddLogEntry(\"Basic 'notify' ONLY message and displayed in GUI if notifications are enabled\", [\"notify\"]);\n\t\n\t// info and notify\n\taddLogEntry(\"Basic 'info and notify' message and displayed in GUI if notifications are enabled\", [\"info\", \"notify\"]);\n\t\n\t// log file only\n\taddLogEntry(\"Information sent to the log file only, and only if logging to a file is enabled\", [\"logFileOnly\"]);\n\t\n\t// Console only (session based upload|download)\n\taddLogEntry(\"Basic 'Console only with new line' message\", [\"consoleOnly\"]);\n\t\n\t// Console only with no new line\n\taddLogEntry(\"Basic 'Console only with no new line' message\", [\"consoleOnlyNoNewLine\"]);\n\n```\n\n### Documentation Updates\nIf 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.\n\n## Development Testing\nWhilst there are more modern D compilers available, ensuring client build compatibility with older platforms is a key requirement.\n\nThe 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.\n\nThe 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.\n\nIt is assumed here that you know how to download and install the correct LDC compiler for your platform.\n\n## Submitting a PR\nWhen submitting a PR, please provide your testing evidence in the PR submission of what has been fixed, in the format of:\n\n### Without PR\n```\nApplication output that is doing whatever | or illustration of issue | illustration of bug\n```\n\n### With PR\n```\nApplication output that is doing whatever | or illustration of issue being fixed | illustration of bug being fixed\n```\nPlease also include validation of compilation using the minimum LDC package version.\n\nTo assist with your testing validation against the minimum LDC compiler version, a script as per below could assist you with this validation:\n\n```bash\n\n#!/bin/bash\n  \nPR=<Your_PR_Number>\n\nrm -rf ./onedrive-pr${PR}\ngit clone https://github.com/abraunegg/onedrive.git onedrive-pr${PR}\ncd onedrive-pr${PR}\ngit fetch origin pull/${PR}/head:pr${PR}\ngit checkout pr${PR}\n\n# MIN LDC Version to compile\n# MIN Version for ARM / Compiling with LDC\nsource ~/dlang/ldc-1.20.1/activate\n\n# Compile code with specific LDC version\n./configure --enable-debug --enable-notifications; make clean; make;\ndeactivate\n./onedrive --version\n\n```\n\n## References\n\n* D Language Official Style Guide: https://dlang.org/dstyle.html\n* British English spelling conventions: https://www.collinsdictionary.com/\n"
  },
  {
    "path": "docs/docker.md",
    "content": "# Run the OneDrive Client for Linux under Docker\nThis client can be run as a Docker container, with 3 available container base options for you to choose from:\n\n| Container Base | Docker Tag  | Description                                                    | i686 | x86_64 | ARMHF | AARCH64 |\n|----------------|-------------|----------------------------------------------------------------|:------:|:------:|:-----:|:-------:|\n| Alpine Linux   | edge-alpine | Docker container based on Alpine 3.23 using 'master'           |❌|✔|❌|✔|\n| Alpine Linux   | alpine      | Docker container based on Alpine 3.23 using latest release     |❌|✔|❌|✔|\n| Debian         | debian      | Docker container based on Debian 13 using latest release       |✔|✔|✔|✔|\n| Debian         | edge        | Docker container based on Debian 13 using 'master'             |✔|✔|✔|✔|\n| Debian         | edge-debian | Docker container based on Debian 13 using 'master'             |✔|✔|✔|✔|\n| Debian         | latest      | Docker container based on Debian 13 using latest release       |✔|✔|✔|✔|\n| Fedora         | edge-fedora | Docker container based on Fedora 43 using 'master'             |❌|✔|❌|✔|\n| Fedora         | fedora      | Docker container based on Fedora 43 using latest release       |❌|✔|❌|✔|\n\nThese containers offer a simple monitoring-mode service for the OneDrive Client for Linux.\n\nThe instructions below have been validated on:\n*   Fedora 40\n\nThe 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.\n\nThe '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'.\n\nAdditionally 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.\n\n> [!NOTE]\n> The below instructions for docker has been tested and validated when logging into the system as an unprivileged user (non 'root' user).\n\n## High Level Configuration Steps\n1. Install 'docker' as per your distribution platform's instructions if not already installed.\n2. Configure 'docker' to allow non-privileged users to run Docker commands\n3. Disable 'SELinux' as per your distribution platform's instructions\n4. Test 'docker' by running a test container without using `sudo`\n5. Prepare the required docker volumes to store the configuration and data\n6. Run the 'onedrive' container and perform authorisation\n7. Running the 'onedrive' container under 'docker'\n\n## Configuration Steps\n\n### 1. Install 'docker' on your platform\nInstall Docker for your system using the official instructions found at https://docs.docker.com/engine/install/.\n\n> [!CAUTION]\n> 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.\n\n\n### 2. Configure 'docker' to allow non-privileged users to run Docker commands\nRead 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.\n\n### 3. Disable SELinux on your platform\nIn 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:\n```text\nERROR: The local file system returned an error with the following message:\n  Error Message:    /onedrive/conf/refresh_token: Permission denied\n\nThe database cannot be opened. Please check the permissions of ~/.config/onedrive/items.sqlite3\n```\nThe 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.\n\n* Fedora: https://docs.fedoraproject.org/en-US/quick-docs/selinux-changing-states-and-modes/#_disabling_selinux\n* Red Hat Enterprise Linux: https://access.redhat.com/solutions/3176\n\nPost disabling SELinux and reboot your system, confirm that `getenforce` returns `Disabled`:\n```text\n$ getenforce\nDisabled\n```\n\nIf you are still experiencing permission issues despite disabling SELinux, please read https://www.redhat.com/sysadmin/container-permission-denied-errors\n\n### 4. Test 'docker' on your platform\nEnsure that 'docker' is running as a system service, and is enabled to be activated on system reboot:\n```bash\nsudo systemctl enable --now docker\n```\n\nTest that 'docker' is operational for your 'non-root' user, as per below:\n```bash\n[alex@fedora-40-docker-host ~]$ docker run hello-world\nUnable to find image 'hello-world:latest' locally\nlatest: Pulling from library/hello-world\n719385e32844: Pull complete \nDigest: sha256:88ec0acaa3ec199d3b7eaf73588f4518c25f9d34f58ce9a0df68429c5af48e8d\nStatus: Downloaded newer image for hello-world:latest\n\nHello from Docker!\nThis message shows that your installation appears to be working correctly.\n\nTo generate this message, Docker took the following steps:\n 1. The Docker client contacted the Docker daemon.\n 2. The Docker daemon pulled the \"hello-world\" image from the Docker Hub.\n    (amd64)\n 3. The Docker daemon created a new container from that image which runs the\n    executable that produces the output you are currently reading.\n 4. The Docker daemon streamed that output to the Docker client, which sent it\n    to your terminal.\n\nTo try something more ambitious, you can run an Ubuntu container with:\n $ docker run -it ubuntu bash\n\nShare images, automate workflows, and more with a free Docker ID:\n https://hub.docker.com/\n\nFor more examples and ideas, visit:\n https://docs.docker.com/get-started/\n\n[alex@fedora-40-docker-host ~]$ \n```\n\n### 5. Configure the required docker volumes\nThe 'onedrive' Docker container requires 2 docker volumes to operate:\n*    Config Volume\n*    Data Volume\n\nThe 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`\n\nThe 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`\n\n#### 5.1 Prepare the 'config' volume\nCreate the 'config' volume with the following command:\n```bash\ndocker volume create onedrive_conf\n```\n\nThis 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.\n\n#### 5.2 Prepare the 'data' volume\nCreate the 'data' volume with the following command:\n```bash\ndocker volume create onedrive_data\n```\n\nThis 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:\n\n*   The owner of this specified folder must not be root\n*   The owner of this specified folder must have permissions for its parent directory\n*   Docker will attempt to change the permissions of the volume to the user the container is configured to run as\n\n> [!IMPORTANT]\n> 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:\n> ```bash\n> ROOT level privileges prohibited!\n> ```\n\n### 6. First run of Docker container under docker and performing authorisation\nThe 'onedrive' client within the container first needs to be authorised with your Microsoft account. This is achieved by initially running docker in interactive mode.\n\nRun 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\"`).\n\n> [!IMPORTANT]\n> 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.\n\nIt 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.\n```bash\nexport ONEDRIVE_DATA_DIR=\"${HOME}/OneDrive\"\nexport ONEDRIVE_UID=`id -u`\nexport ONEDRIVE_GID=`id -g`\nmkdir -p ${ONEDRIVE_DATA_DIR}\ndocker run -it --name onedrive -v onedrive_conf:/onedrive/conf \\\n    -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" \\\n    -e \"ONEDRIVE_UID=${ONEDRIVE_UID}\" \\\n    -e \"ONEDRIVE_GID=${ONEDRIVE_GID}\" \\\n    driveone/onedrive:edge\n```\n\nWhen the Docker container successfully starts:\n*   You will be asked to open a specific link using your web browser \n*   Login to your Microsoft Account and give the application the permission \n*   After giving the permission, you will be redirected to a blank page\n*   Copy the URI of the blank page into the application prompt to authorise the application\n\nOnce 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.\n\nIf the client is working as expected, you can detach from the container with CTRL+P, CTRL+Q.\n\n#### 6.1. Read-Only / Upload-Only Sync Scenarios\nIf you are running the Docker container in upload-only mode and want to ensure that the OneDrive client cannot modify the original source files, the data directory must be mounted as read-only.\n\nThis is controlled at the container mount level, not by ownership (chown) or permissions (chmod) inside the container.\n\nIf this is your desired configuration, you must mount your 'ONEDRIVE_DATA_DIR' with read-only permissions to ensure your data source is immutable and cannot be changed. Augment the above script in the following manner:\n```bash\nexport ONEDRIVE_DATA_DIR=\"${HOME}/OneDrive\"\nexport ONEDRIVE_UID=`id -u`\nexport ONEDRIVE_GID=`id -g`\nmkdir -p ${ONEDRIVE_DATA_DIR}\ndocker run -it --name onedrive -v onedrive_conf:/onedrive/conf \\\n    -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data:ro\" \\\n    -e \"ONEDRIVE_UID=${ONEDRIVE_UID}\" \\\n    -e \"ONEDRIVE_GID=${ONEDRIVE_GID}\" \\\n\t-e ONEDRIVE_UPLOADONLY=1 \\\n    driveone/onedrive:edge\n```\n> [!NOTE]\n> Essentially, any Docker command where you are mounting your 'ONEDRIVE_DATA_DIR', you need to append `:ro` to the `/onedrive/data` specification to ensure your data directory is mounted in Docker as read-only volume.\n\n### 7. Running the 'onedrive' container under 'docker'\n\n#### 7.1 Check if the monitor service is running\n```bash\ndocker ps -f name=onedrive\n```\n\n#### 7.2 Show 'onedrive' runtime logs\n```bash\ndocker logs onedrive\n```\n\n#### 7.3 Stop running 'onedrive' container\n```bash\ndocker stop onedrive\n```\n\n#### 7.4 Start 'onedrive' container\n```bash\ndocker start onedrive\n```\n\n#### 7.5 Remove 'onedrive' container\n```bash\ndocker rm -f onedrive\n```\n\n### Customising OneDrive Runtime Behaviour in Docker\n\nWhen running the OneDrive client inside Docker, the container **always starts** via `entrypoint.sh`, which ensures that the following arguments are added automatically:\n\n```\n--confdir /onedrive/conf --syncdir /onedrive/data\n```\n\nThis design guarantees that:\n\n* Your configuration files persist in the `/onedrive/conf` volume.\n* Your synchronised data persists in the `/onedrive/data` volume.\n* The container behaves consistently across hosts, upgrades, and architectures.\n\nBecause these arguments are always supplied, any `sync_dir` or `confdir` values defined in the configuration file are **overridden at runtime by design**. This avoids confusion and ensures predictable behaviour. These specific paths are the bind-mounts between container and host and should **not be changed manually**.\n\n#### Default Docker volume behaviour\nBy default, Docker bind mounts and volumes are mounted read-write inside the container. This means that, unless explicitly restricted, the container process may create, modify, rename, or delete files within the mounted directory, subject to normal filesystem permissions.\n\n#### Using read-only mounts\nIf you want to prevent the container from modifying the mounted data (for example, in upload-only or backup-style scenarios), the bind mount must be explicitly marked as read-only using the `:ro` mount option. A read-only mount enforces immutability at the container boundary and cannot be overridden from inside the container, regardless of ownership or permissions.\n\n### Supported ways to customise runtime behaviour\n\nThere are **two supported mechanisms** for adjusting how the client runs inside Docker:\n\n1. **Docker environment variables**\n   Many client options are exposed as environment variables in a reproducible way. For example:\n\n   ```shell\n   -e ONEDRIVE_DOWNLOADONLY=1\n   -e ONEDRIVE_SYNC_ONCE=1\n   -e ONEDRIVE_VERBOSE=1\n   ```\n\n   See the full list here:\n   👉 [Supported Docker environment variables](https://github.com/abraunegg/onedrive/blob/master/docs/docker.md#supported-docker-environment-variables)\n\n2. **Configuration file inside `/onedrive/conf`**\n   For permanent or advanced options not covered by environment variables, you can create or edit the client configuration file in the mounted config directory.\n   Documentation:\n   👉 [Editing the running configuration and using a config file](https://github.com/abraunegg/onedrive/blob/master/docs/docker.md#editing-the-running-configuration-and-using-a-config-file)\n\n> [!IMPORTANT]\n> **Do not manually add `--syncdir` or `--confdir`** when overriding the container command.\n>\n> If you do:\n> \n> * You bypass the `entrypoint.sh` logic that manages UID/GID mapping, privilege dropping, and environment translation.\n> * You risk syncing data to the wrong location (`~/OneDrive` inside the container) or creating incorrect file ownership on the host.\n> \n> Instead:\n> \n> * Use existing **Docker environment variables** for controling specific application functionality.\n> * Use a **config file** and or 'sync_list' file inside `/onedrive/conf` for advanced configuration.\n\n\n### How to use Docker-compose\nYou can utilise `docker-compose` if available on your platform if you are able to use docker compose schemas > 3.\n\nIn the following example it is assumed you have a `ONEDRIVE_DATA_DIR` environment variable and have already created the `onedrive_conf` volume. \n\nYou can also use docker bind mounts for the configuration folder, e.g. `export ONEDRIVE_CONF=\"${HOME}/OneDriveConfig\"`.\n\n```\nversion: \"3\"\nservices:\n    onedrive:\n        image: driveone/onedrive:edge\n        restart: unless-stopped\n        environment:\n            - ONEDRIVE_UID=${PUID}\n            - ONEDRIVE_GID=${PGID}\n        volumes: \n            - onedrive_conf:/onedrive/conf\n            - ${ONEDRIVE_DATA_DIR}:/onedrive/data\n```\n\n> [!IMPORTANT]\n> 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.\n> 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.\n\n### Editing the running configuration and using a 'config' file\nThe '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)  \nThen put it into your onedrive_conf volume path, which can be found with:  \n\n```bash\ndocker volume inspect onedrive_conf\n```\n\nOr 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.\n\nThe 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)\n\n### Syncing multiple accounts\nThere are many ways to do this, the easiest is probably to do the following:\n1. Create a second docker config volume (replace `Work` with your desired name):  `docker volume create onedrive_conf_Work`\n2. And start a second docker monitor container (again replace `Work` with your desired name):\n```\nexport ONEDRIVE_DATA_DIR_WORK=\"/home/abraunegg/OneDriveWork\"\nmkdir -p ${ONEDRIVE_DATA_DIR_WORK}\ndocker run -it --restart unless-stopped --name onedrive_Work -v onedrive_conf_Work:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR_WORK}:/onedrive/data\" driveone/onedrive:edge\n```\n\n### Run or update the Docker container with one script\nIf you are experienced with docker and onedrive, you can use the following script:\n\n```bash\n# Update ONEDRIVE_DATA_DIR with correct OneDrive directory path\nONEDRIVE_DATA_DIR=\"${HOME}/OneDrive\"\n# Create directory if non-existent\nmkdir -p ${ONEDRIVE_DATA_DIR} \n\nfirstRun='-d'\ndocker pull driveone/onedrive:edge\ndocker inspect onedrive_conf > /dev/null 2>&1 || { docker volume create onedrive_conf; firstRun='-it'; }\ndocker inspect onedrive > /dev/null 2>&1 && docker rm -f onedrive\ndocker run $firstRun --restart unless-stopped --name onedrive -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n\n## Supported Docker Environment Variables\n| Variable | Purpose | Sample Value |\n| ---------------- | --------------------------------------------------- |:--------------------------------------------------------------------------------------------------------------------------------:|\n| <B>ONEDRIVE_UID</B> | UserID (UID) to run as  | 1000 |\n| <B>ONEDRIVE_GID</B> | GroupID (GID) to run as | 1000 |\n| <B>ONEDRIVE_VERBOSE</B> | Controls \"--verbose\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_DEBUG</B> | Controls \"--verbose --verbose\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_DEBUG_HTTPS</B> | Controls \"--debug-https\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_RESYNC</B> | Controls \"--resync\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_DOWNLOADONLY</B> | Controls \"--download-only\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_CLEANUPLOCAL</B> | Controls \"--cleanup-local-files\" to cleanup local files and folders if they are removed online. Default is 0 | 1 |\n| <B>ONEDRIVE_UPLOADONLY</B> | Controls \"--upload-only\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_NOREMOTEDELETE</B> | Controls \"--no-remote-delete\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_LOGOUT</B> | Controls \"--logout\" switch. Default is 0 | 1 |\n| <B>ONEDRIVE_REAUTH</B> | Controls \"--reauth\" switch. Default is 0 | 1 |\n| <B>ONEDRIVE_AUTHFILES</B> | Controls \"--auth-files\" option. Default is \"\" | Please read [CLI Option: --auth-files](./application-config-options.md#cli-option---auth-files) |\n| <B>ONEDRIVE_AUTHRESPONSE</B> | Controls \"--auth-response\" option. Default is \"\" | Please read [CLI Option: --auth-response](./application-config-options.md#cli-option---auth-response) |\n| <B>ONEDRIVE_DISPLAY_CONFIG</B> | Controls \"--display-running-config\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_SINGLE_DIRECTORY</B> | Controls \"--single-directory\" option. Default = \"\" | \"mydir\" |\n| <B>ONEDRIVE_DRYRUN</B> | Controls \"--dry-run\" option. Default is 0 | 1 |\n| <B>ONEDRIVE_DISABLE_DOWNLOAD_VALIDATION</B> | Controls \"--disable-download-validation\" option. Default is 0 | 1 |\n| <B>ONEDRIVE_DISABLE_UPLOAD_VALIDATION</B> | Controls \"--disable-upload-validation\" option. Default is 0 | 1 |\n| <B>ONEDRIVE_SYNC_SHARED_FILES</B> | Controls \"--sync-shared-files\" option. Default is 0 | 1 |\n| <B>ONEDRIVE_RUNAS_ROOT</B> | Controls if the Docker container should be run as the 'root' user instead of 'onedrive' user. Default is 0 | 1 |\n| <B>ONEDRIVE_SYNC_ONCE</B> | Controls if the Docker container should be run in Standalone Mode. It will use Monitor Mode otherwise. Default is 0 | 1 |\n| <B>ONEDRIVE_FILE_FRAGMENT_SIZE</B> | Controls the fragment size when uploading large files to Microsoft OneDrive. The value specified is in MB. Default is 10, Limit is 60 | 25 |\n| <B>ONEDRIVE_THREADS</B> | Controls the value for the number of worker threads used for parallel upload and download operations. Default is 8, Limit is 16 | 4 |\n\n### Environment Variables Usage Examples\n**Verbose Output:**\n```bash\ndocker container run -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n**Debug Output:**\n```bash\ndocker container run -e ONEDRIVE_DEBUG=1 -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n**Perform a --resync:**\n```bash\ndocker container run -e ONEDRIVE_RESYNC=1 -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n**Perform a --resync and --verbose:**\n```bash\ndocker container run -e ONEDRIVE_RESYNC=1 -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n**Perform a --logout:**\n```bash\ndocker container run -it -e ONEDRIVE_LOGOUT=1 -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n**Perform a --logout and re-authenticate:**\n```bash\ndocker container run -it -e ONEDRIVE_REAUTH=1 -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n**Perform a sync using ONEDRIVE_SINGLE_DIRECTORY:**\n```bash\ndocker container run -e ONEDRIVE_SINGLE_DIRECTORY=\"path/which/needs/to/be/synced\" -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n**Perform a sync specifying UID and GID:**\n```bash\ndocker container run -e ONEDRIVE_UID=9999 -e ONEDRIVE_GID=9999 -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" driveone/onedrive:edge\n```\n\n> [!IMPORTANT]\n> Is using a Docker Environment Variable that requires you to specify a 'path' (ONEDRIVE_AUTHFILES, ONEDRIVE_AUTHRESPONSE, ONEDRIVE_SINGLE_DIRECTORY), the placement of quotes around the path is critically important.\n>\n> Please ensure you are formatting the option correctly:\n>```\n> -e OPTION=\"path/which/needs/to/be/synced\"\n>```\n> Please also ensure that the path specified complies with the actual application usage argument. Please read the relevant config option advice in the [CLI Option Documentation](./application-config-options.md)\n\n## Building a custom Docker image\n\n### Build Environment Requirements\n*   Build environment must have at least 1GB of memory & 2GB swap space\n\nYou can validate your build environment memory status with the following command:\n```text\ncat /proc/meminfo | grep -E 'MemFree|Swap'\n```\nThis should result in the following similar output:\n```text\nMemFree:         3704644 kB\nSwapCached:            0 kB\nSwapTotal:       8117244 kB\nSwapFree:        8117244 kB\n```\n\nIf you do not have enough swap space, you can use the following script to dynamically allocate a swapfile for building the Docker container:\n\n```bash\ncd /var \nsudo fallocate -l 1.5G swapfile\nsudo chmod 600 swapfile\nsudo mkswap swapfile\nsudo swapon swapfile\n# make swap permanent\nsudo nano /etc/fstab\n# add \"/swapfile swap swap defaults 0 0\" at the end of file\n# check it has been assigned\nswapon -s\nfree -h\n```\n\nIf you are running a Raspberry Pi, you will need to edit your system configuration to increase your swapfile:\n\n*   Modify the file `/etc/dphys-swapfile` and edit the `CONF_SWAPSIZE`, for example: `CONF_SWAPSIZE=2048`. \n\n> [!IMPORTANT]\n> A reboot of your Raspberry Pi is required to make this change effective.\n\n### Building and running a custom Docker image\nYou can also build your own image instead of pulling the one from [hub.docker.com](https://hub.docker.com/r/driveone/onedrive):\n```bash\ngit clone https://github.com/abraunegg/onedrive\ncd onedrive\ndocker build . -t local-onedrive -f contrib/docker/Dockerfile\ndocker container run -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" local-onedrive:latest\n```\n\nThere 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.\n\n### How to build and run a custom Docker image based on Debian\n``` bash\ndocker build . -t local-onedrive-debian -f contrib/docker/Dockerfile-debian\ndocker container run -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" local-onedrive-debian:latest\n```\n\n### How to build and run a custom Docker image based on Alpine Linux\n``` bash\ndocker build . -t local-onedrive-alpine -f contrib/docker/Dockerfile-alpine\ndocker container run -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" local-onedrive-alpine:latest\n```\n\n### How to build and run a custom Docker image for ARMHF (Raspberry Pi)\nCompatible with:\n*    Raspberry Pi\n*    Raspberry Pi 2\n*    Raspberry Pi Zero\n*    Raspberry Pi 3\n*    Raspberry Pi 4\n``` bash\ndocker build . -t local-onedrive-armhf -f contrib/docker/Dockerfile-debian\ndocker container run -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" local-onedrive-armhf:latest\n```\n\n### How to build and run a custom Docker image for AARCH64 Platforms\n``` bash\ndocker build . -t local-onedrive-aarch64 -f contrib/docker/Dockerfile-debian\ndocker container run -v onedrive_conf:/onedrive/conf -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data\" local-onedrive-aarch64:latest\n```\n### How to support double-byte languages\nIn 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:\n```\nFROM driveone/onedrive\n\nENV DEBIAN_FRONTEND noninteractive\n\nRUN apt-get update\nRUN apt-get install -y locales\n\nRUN echo \"ja_JP.UTF-8 UTF-8\" > /etc/locale.gen && \\\n    locale-gen ja_JP.UTF-8 && \\\n    dpkg-reconfigure locales && \\\n    /usr/sbin/update-locale LANG=ja_JP.UTF-8\n\nENV LC_ALL ja_JP.UTF-8\n```\nThe above example changes the Docker container to support Japanese. To support your local language, change `ja_JP.UTF-8` to the required entry.\n"
  },
  {
    "path": "docs/install.md",
    "content": "# Installing or Upgrading the OneDrive Client for Linux\n\n## Table of Contents\n\n- [Recommended Installation Method (Using Pre-Built Packages)](#recommended-installation-method-using-pre-built-packages)\n  - [Important Notice for all Debian \\| Ubuntu \\| Linux Mint \\| Pop!_OS \\| Raspbian \\| Zorin Users](#important-notice-for-all-debian--ubuntu--linux-mint--pop_os--raspbian--zorin-users)\n  - [Which Installation Method Should I Use?](#which-installation-method-should-i-use)\n  - [When Should You Build From Source?](#when-should-you-build-from-source)\n\n- [Building from Source](#building-from-source)\n  - [Minimum Build Requirements](#minimum-build-requirements)\n  - [Install Build Dependencies (By Distribution)](#install-build-dependencies-by-distribution)\n\n- [Clone, Configure, Build, Install](#clone-configure-build-install)\n  - [High Level Steps to building the OneDrive Client for Linux](#high-level-steps-to-building-the-onedrive-client-for-linux)\n  - [Building the Application Using Default configure Settings](#building-the-application-using-default-configure-settings)\n  - [Build Options for Customising the Application](#build-options-for-customising-the-application)\n\n- [Upgrading the Client](#upgrading-the-client)\n  - [If installed from a distribution package](#if-installed-from-a-distribution-package)\n  - [If installed from source](#if-installed-from-source)\n\n- [Uninstalling the client](#uninstalling-the-client)\n  - [If installed from a distribution package](#if-installed-from-a-distribution-package-1)\n  - [If installed from source](#if-installed-from-source-1)\n\n\n## Overview\nThis document explains how to install or upgrade the OneDrive Client for Linux.\n\nThe preferred installation method is to use pre-built distribution packages wherever they are available and current. On some distributions, particularly Debian, Ubuntu, Linux Mint, and Raspberry Pi OS, the versions provided in the default distribution repositories are outdated and unsupported. These must not be used.\n\nIf your distribution provides a current maintained package, you should install the client from your package manager. If your distribution does not provide a supported package, or you need to build the client for a custom or minimal environment, building from source is supported and documented below.\n\nBefore continuing, identify your Linux distribution and follow the installation path appropriate to your system.\n\n## Recommended Installation Method (Using Pre-Built Packages)\n\n### Important Notice for all Debian | Ubuntu | Linux Mint | Pop!_OS | Raspbian | Zorin Users\n\n> [!IMPORTANT]\n> **Do NOT install the OneDrive client from your distribution’s default repositories.** These packaged versions are **outdated, unsupported, and contain known defects.**\n>\n> Instead, install the **fully supported and actively maintained version** from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md)\n\n\n### Which Installation Method Should I Use?\n\n| Distribution & Version                 | Distribution Package Name & Link                                                                         | Distribution Package Version | Correct Installation Method |\n|----------------------------------------|----------------------------------------------------------------------------------------------------------|:----------------------------------------------------:|-----------------------------|\n| Alpine Linux                           | [onedrive](https://pkgs.alpinelinux.org/packages?name=onedrive&branch=edge)                              |<a href=\"https://pkgs.alpinelinux.org/packages?name=onedrive&branch=edge\"><img src=\"https://repology.org/badge/version-for-repo/alpine_edge/onedrive.svg?header=\" alt=\"Alpine Linux Edge package\" width=\"46\" height=\"20\"></a> | Alpine **Stable** may ship older versions. If your version is outdated, you need to build from source |\n| Arch Linux<br><br>Manjaro Linux        | [onedrive-abraunegg](https://aur.archlinux.org/packages/onedrive-abraunegg/)                             |<a href=\"https://aur.archlinux.org/packages/onedrive-abraunegg\"><img src=\"https://repology.org/badge/version-for-repo/aur/onedrive-abraunegg.svg?header=\" alt=\"AUR package\" width=\"46\" height=\"20\"></a>| Install via: `pamac build onedrive-abraunegg` from the Arch Linux User Repository (AUR)<br><br>**Note:** You must first install 'base-devel' as this is a pre-requisite for using the AUR<br><br>**Note:** If asked regarding a provider for 'd-runtime' and 'd-compiler', select 'liblphobos' and 'ldc'<br><br>**Note:** System must have at least 1GB of memory & 1GB swap space<br><br>AUR package `onedrive-abraunegg` follows the release versions<br>AUR package `onedrive-abraunegg-git` follows the 'master' branch |\n| CentOS Stream 8                        | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044)                              |<a href=\"https://koji.fedoraproject.org/koji/packageinfo?packageID=26044\"><img src=\"https://repology.org/badge/version-for-repo/epel_8/onedrive.svg?header=\" alt=\"EPEL 8 package\" width=\"46\" height=\"20\"></a>| **Note:** You must install and enable the EPEL Repository first.<br><br>Install via: `sudo dnf install onedrive` |\n| CentOS Stream 9                        | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044)                              |<a href=\"https://koji.fedoraproject.org/koji/packageinfo?packageID=26044\"><img src=\"https://repology.org/badge/version-for-repo/epel_9/onedrive.svg?header=\" alt=\"EPEL 9 package\" width=\"46\" height=\"20\"></a>| **Note:** You must install and enable the EPEL Repository first.<br><br>Install via: `sudo dnf install onedrive` |\n| CentOS Stream 10                       | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044)                              |<a href=\"https://koji.fedoraproject.org/koji/packageinfo?packageID=26044\"><img src=\"https://repology.org/badge/version-for-repo/epel_10/onedrive.svg?header=\" alt=\"EPEL 10 package\" width=\"46\" height=\"20\"></a>| **Note:** You must install and enable the EPEL Repository first.<br><br>Install via: `sudo dnf install onedrive` |\n| Debian 11                              | [onedrive](https://packages.debian.org/bullseye/source/onedrive)                                         |<a href=\"https://packages.debian.org/bullseye/source/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/debian_11/onedrive.svg?header=\" alt=\"Debian 11 package\" width=\"46\" height=\"20\"></a>| Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| Debian 12                              | [onedrive](https://packages.debian.org/bookworm/source/onedrive)                                         |<a href=\"https://packages.debian.org/bookworm/source/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/debian_12/onedrive.svg?header=\" alt=\"Debian 12 package\" width=\"46\" height=\"20\"></a>| Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| Debian 13                              | [onedrive](https://packages.debian.org/trixie/source/onedrive)                                           |<a href=\"https://packages.debian.org/bookworm/source/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/debian_13/onedrive.svg?header=\" alt=\"Debian 13 package\" width=\"46\" height=\"20\"></a>| Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| Debian Sid                             | [onedrive](https://packages.debian.org/sid/onedrive)                                                     |<a href=\"https://packages.debian.org/sid/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/debian_unstable/onedrive.svg?header=\" alt=\"Debian Sid package\" width=\"46\" height=\"20\"></a>| Install via: `sudo apt install --no-install-recommends --no-install-suggests onedrive` |\n| Fedora                                 | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044)                              |<a href=\"https://koji.fedoraproject.org/koji/packageinfo?packageID=26044\"><img src=\"https://repology.org/badge/version-for-repo/fedora_rawhide/onedrive.svg?header=\" alt=\"Fedora Rawhide package\" width=\"46\" height=\"20\"></a>| Install via: `sudo dnf install onedrive` |\n| FreeBSD                                | [onedrive](https://www.freshports.org/net/onedrive)                                                      |<a href=\"https://www.freshports.org/net/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/freebsd/onedrive.svg?header=\" alt=\"FreeBSD package\" width=\"46\" height=\"20\"></a>| Install via: `pkg install onedrive` |\n| Gentoo                                 | [onedrive](https://packages.gentoo.org/packages/net-misc/onedrive)                                       |<a href=\"https://packages.gentoo.org/packages/net-misc/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/gentoo/onedrive.svg?header=\" alt=\"Gentoo package\" width=\"46\" height=\"20\"></a>| Install via: `sudo emerge net-misc/onedrive` |\n| Homebrew                               | [onedrive-cli](https://formulae.brew.sh/formula/onedrive-cli)                                            |<a href=\"https://formulae.brew.sh/formula/onedrive-cli\"><img src=\"https://repology.org/badge/version-for-repo/homebrew/onedrive-cli.svg?header=\" alt=\"Homebrew package\" width=\"46\" height=\"20\"></a> | Install via: `brew install onedrive-cli` |\n| Linux Mint 21.x                        | [onedrive](https://community.linuxmint.com/software/view/onedrive)                                       |<a href=\"https://community.linuxmint.com/software/view/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/ubuntu_22_04/onedrive.svg?header=\" alt=\"Ubuntu 22.04 package\" width=\"46\" height=\"20\"></a> | Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| Linux Mint 22.x                        | [onedrive](https://community.linuxmint.com/software/view/onedrive)                                       |<a href=\"https://community.linuxmint.com/software/view/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/ubuntu_24_04/onedrive.svg?header=\" alt=\"Ubuntu 24.04 package\" width=\"46\" height=\"20\"></a> | Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| Linux Mint Debian Edition 6            | [onedrive](https://community.linuxmint.com/software/view/onedrive)                                       |<a href=\"https://packages.debian.org/bookworm/source/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/debian_12/onedrive.svg?header=\" alt=\"Debian 12 package\" width=\"46\" height=\"20\"></a>| Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| Linux Mint Debian Edition 7            | [onedrive](https://community.linuxmint.com/software/view/onedrive)                                       |<a href=\"https://packages.debian.org/bookworm/source/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/debian_13/onedrive.svg?header=\" alt=\"Debian 13 package\" width=\"46\" height=\"20\"></a>| Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| NixOS                                  | [onedrive](https://search.nixos.org/packages?channel=25.05&query=onedrive)                               |<a href=\"https://search.nixos.org/packages?channel=25.05&query=onedrive\"><img src=\"https://repology.org/badge/version-for-repo/nix_unstable/onedrive.svg?header=\" alt=\"nixpkgs unstable package\" width=\"46\" height=\"20\"></a>| Install via: `nix-env -iA nixpkgs.onedrive` **or** `services.onedrive.enable = true` in `configuration.nix` |\n| MX Linux 25                            | [onedrive](https://mxrepo.com/mx/repo/pool/main/o/onedrive/)                                             |<a href=\"https://mxrepo.com/mx/repo/pool/main/o/onedrive/\"><img src=\"https://repology.org/badge/version-for-repo/mx_25/onedrive.svg?header=\" alt=\"MX Linux package\" width=\"46\" height=\"20\"></a>| Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| OpenSUSE                               | [onedrive](https://software.opensuse.org/package/onedrive)                                               |<a href=\"https://software.opensuse.org/package/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/opensuse_network_tumbleweed/onedrive.svg?header=\" alt=\"openSUSE Tumbleweed package\" width=\"46\" height=\"20\"></a>| Install via: `sudo zypper install onedrive` |\n| OpenSUSE Build Service                 | [onedrive](https://build.opensuse.org/package/show/home:npreining:debian-ubuntu-onedrive/onedrive)       | No API available for version information | |\n| Raspbian                               | [onedrive](https://archive.raspbian.org/raspbian/pool/main/o/onedrive/)                                  |<a href=\"https://archive.raspbian.org/raspbian/pool/main/o/onedrive/\"><img src=\"https://repology.org/badge/version-for-repo/raspbian_stable/onedrive.svg?header=\" alt=\"Raspbian Stable package\" width=\"46\" height=\"20\"></a> | Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| RedHat Enterprise Linux 8              | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044)                              |<a href=\"https://koji.fedoraproject.org/koji/packageinfo?packageID=26044\"><img src=\"https://repology.org/badge/version-for-repo/epel_8/onedrive.svg?header=\" alt=\"EPEL 8 package\" width=\"46\" height=\"20\"></a>| **Note:** You must install and enable the EPEL Repository first.<br><br>Install via: `sudo dnf install onedrive` |\n| RedHat Enterprise Linux 9              | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044)                              |<a href=\"https://koji.fedoraproject.org/koji/packageinfo?packageID=26044\"><img src=\"https://repology.org/badge/version-for-repo/epel_9/onedrive.svg?header=\" alt=\"EPEL 9 package\" width=\"46\" height=\"20\"></a>| **Note:** You must install and enable the EPEL Repository first.<br><br>Install via: `sudo dnf install onedrive` |\n| RedHat Enterprise Linux 10             | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044)                              |<a href=\"https://koji.fedoraproject.org/koji/packageinfo?packageID=26044\"><img src=\"https://repology.org/badge/version-for-repo/epel_10/onedrive.svg?header=\" alt=\"EPEL 10 package\" width=\"46\" height=\"20\"></a>| **Note:** You must install and enable the EPEL Repository first.<br><br>Install via: `sudo dnf install onedrive` |\n| Slackware                              | [onedrive](https://slackbuilds.org/result/?search=onedrive&sv=)                                          |<a href=\"https://slackbuilds.org/result/?search=onedrive&sv=\"><img src=\"https://repology.org/badge/version-for-repo/slackbuilds/onedrive.svg?header=\" alt=\"SlackBuilds package\" width=\"46\" height=\"20\"></a>| Install via SlackBuilds: https://slackbuilds.org/result/?search=onedrive |\n| Solus                                  | [onedrive](https://packages.getsol.us/shannon/o/onedrive/?sort=time&order=desc)                          |<a href=\"https://packages.getsol.us/shannon/o/onedrive/?sort=time&order=desc\"><img src=\"https://repology.org/badge/version-for-repo/solus/onedrive.svg?header=\" alt=\"Solus package\" width=\"46\" height=\"20\"></a>| Install via: `sudo eopkg install onedrive` |\n| Ubuntu 22.04 LTS                       | [onedrive](https://packages.ubuntu.com/jammy/onedrive)                                                   |<a href=\"https://packages.ubuntu.com/jammy/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/ubuntu_22_04/onedrive.svg?header=\" alt=\"Ubuntu 22.04 package\" width=\"46\" height=\"20\"></a> | Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n| Ubuntu 24.04 LTS                       | [onedrive](https://packages.ubuntu.com/noble/onedrive)                                                   |<a href=\"https://packages.ubuntu.com/noble/onedrive\"><img src=\"https://repology.org/badge/version-for-repo/ubuntu_24_04/onedrive.svg?header=\" alt=\"Ubuntu 24.04 package\" width=\"46\" height=\"20\"></a> | Install from the openSUSE Build Service (OBS) repository by following the [Ubuntu / Debian Package Installation Guide](ubuntu-package-install.md) |\n\n> [!IMPORTANT]\n> Distribution versions that are considered **End-of-Life (EOL)** are **no longer supported** or tested with current client releases.\n\n> [!IMPORTANT]\n> Distribution package maintainers are volunteers who generously contribute their time to make software available for your system. New releases of the client may take some time to appear in your distribution’s repositories.\n> \n> If you believe a new release is significantly delayed, please contact your distribution’s package maintainer directly to request an update.\n>\n> **Do not open a bug report or discussion about this here**, as we have no control over the packaging process for your distribution.\n\n### When Should You Build From Source?\nYou should only build from source in the following circumstances:\n\n1. You are packaging for a custom or minimal distribution.\n2. Your distribution does not have a package for your to install. Refer to [repology](https://repology.org/project/onedrive/versions) as a source of all 'onedrive' client versions available across tracked distributions.\n3. You require code newer than the latest release or are building a Pull Request to validate a bugfix.\n\nOutside of these 3 reasons, you should not be building the client yourself. You should endeavour where possible to use a pre-built package.\n\n> [!IMPORTANT]\n> If your distribution does not currently offer a packaged version of the client, you should **request that your distribution maintainers package and support it** as part of their official repositories.\n\n\n## Building from Source\nIf you need to build the client from source, follow this high-level process:\n1. Ensure your system meets the [minimum build requirements](#minimum-build-requirements).\n2. Install the necessary build dependencies and a supported D compiler.\n3. Clone the repository, configure the build options, compile, and install the client.\n\n### Minimum Build Requirements\n*   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.\n*   Install the required distribution package dependencies covering the required development tools and development libraries for curl, sqlite and dbus where required.\n*   Install the [Digital Mars D Compiler (DMD)](https://dlang.org/download.html), [LDC – the LLVM-based D Compiler](https://github.com/ldc-developers/ldc), or, at least version 15 of the [GNU D Compiler (GDC)](https://www.gdcproject.org/)\n\n> [!IMPORTANT]\n> To compile this application successfully, the minimum supported versions of each compiler are: DMD **2.091.1**, LDC **1.20.1**, and, GDC **15**. Ensuring compatibility and optimal performance necessitates the use of these specific versions or their more recent updates.\n>\n> You only need 1 compiler installed. You do not need to install DMD, LDC and GDC. Please *pick* the most applicable compiler for your distribution.\n\n#### Installing DMD Compiler\nTo install the DMD Compiler, this can be achieved in the following manner:\n```text\ncurl -fsS https://dlang.org/install.sh | bash -s dmd\n```\n\n> [!NOTE]\n> Note the `source ~/dlang/dmd-X.XXX.X/activate` string as this will be needed later when building the client.\n\n#### Installing LDC Compiler\nTo install the LDC Compiler, this can be achieved in the following manner:\n```text\ncurl -fsS https://dlang.org/install.sh | bash -s ldc\n```\n\n> [!NOTE]\n> Note the `source ~/dlang/ldc-X.XX.X/activate` string as this will be needed later when building the client.\n\n#### Installing GDC Compiler\nYou will need at least GDC version 15. If your distribution's repositories include a suitable version, you can install it from there. Common names for the GDC package are listed on the [GDC website](https://www.gdcproject.org/downloads#linux-distribution-packages). If the package is unavailable or its version is too old, you can try building it from source following [these instructions](https://wiki.dlang.org/GDC/Installation).\n\n### Install Build Dependencies (By Distribution)\n\n#### Arch Linux | Manjaro Linux\n```text\nsudo pacman -S git make pkg-config curl sqlite dbus ldc\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo pacman -S libnotify\n```\n\n#### CentOS 6.x | RHEL 6.x\nCentOS 6.x and RHEL 6.x reached End of Life status on November 30th 2020 and is no longer supported or tested against.\n\n#### CentOS 7.x | RHEL 7.x\nCentOS 7.x and RHEL 7.x reached End of Life status on June 30th 2024 and is no longer supported or tested against.\n\n#### CentOS Stream 8 | CentOS Stream 9 | CentOS Stream 10\n```text\nsudo dnf groupinstall 'Development Tools'\nsudo dnf install libcurl-devel sqlite-devel dbus-devel\ncurl -fsS https://dlang.org/install.sh | bash -s dmd\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo dnf install libnotify-devel\n```\n\n#### Debian 9\nDebian 9 reached the end of its five-year LTS window on July 18th 2020 and is no longer supported or tested against.\n\n#### Debian 10\nDebian 10 reached the end of its five-year LTS window on September 10th 2022 and is no longer supported or tested against.\n\n#### Debian 11 | Debian 12 | Debian 13 | Linux Mint Debian Edition 6 | Linux Mint Debian Edition 7 - x86_64\n ```text\nsudo apt install build-essential\nsudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl systemd-dev libdbus-1-dev\ncurl -fsS https://dlang.org/install.sh | bash -s dmd\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo apt install libnotify-dev\n```\n\n#### Debian 11 | Debian 12 | Debian 13 - ARMHF and ARM64\n> [!NOTE]\n> For Debian ARM platforms it is advisable to use the distribution provided 'ldc' package to ensure compiler consistency.\n\n```text\nsudo apt install build-essential\nsudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc systemd-dev libdbus-1-dev\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo apt install libnotify-dev\n```\n\n#### Fedora\n> [!NOTE]\n> Fedora 41 and above uses **dnf5** which removes some deprecated aliases, specifically 'groupinstall' in this instance.\n\n```text\nsudo dnf group install development-tools\nsudo dnf install libcurl-devel sqlite-devel dbus-devel\n```\nBefore running the dmd install you need to check for the option 'use-keyboxd' in your gnupg common.conf file and comment it out while running the install.\n```text\ncurl -fsS https://dlang.org/install.sh | bash -s dmd\n```\nOr you may get the following error:\n```text\nmyuser@fedora:~$ curl -fsS https://dlang.org/install.sh | bash -s dmd\nDownloading https://dlang.org/d-keyring.gpg\n######################################################################## 100.0%\ngpg: Note: Specified keyrings are ignored due to option \"use-keyboxd\"\ngpg: Signature made Thu 06 Mar 2025 10:45:29 GMT\ngpg:                using RSA key F3F896F3274BBD9BBBA59058710592E7FB7AF6CA\ngpg: Can't check signature: No public key\nInvalid signature https://dlang.org/d-keyring.gpg.sig\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo dnf install libnotify-devel\n```\n\n#### FreeBSD\n> [!NOTE]\n> Install the required FreeBSD packages as 'root' unless you have installed 'sudo'\n> \n> For FreeBSD it is advisable to use the distribution provided 'ldc' package to ensure compiler consistency.\n\n```text\npkg install bash bash-completion gmake pkgconf autoconf automake logrotate libinotify git sqlite3 ldc\n```\nFor GUI notifications the following is also necessary:\n```text\npkg install libnotify\n```\n\n#### Gentoo\n```text\nsudo emerge --onlydeps net-misc/onedrive\n```\n\n#### MX Linux 25\n ```text\nsudo apt install build-essential\nsudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl systemd-dev libdbus-1-dev\ncurl -fsS https://dlang.org/install.sh | bash -s dmd\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo apt install libnotify-dev\n```\n\n#### OpenSUSE Leap | OpenSUSE Tumbleweed\n```text\nsudo zypper refresh\nsudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static dbus-1-devel\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo zypper install libnotify-devel\n```\n\n#### Raspbian - ARMHF and ARM64\n> [!CAUTION]\n> The minimum LDC compiler version required to compile this application is 1.20.1, 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.\n\n> [!NOTE]\n> These dependencies were validated using:\n> *   `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)\n> *   `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)\n> *   `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)\n\n```text\nsudo apt install build-essential\nsudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc systemd-dev libdbus-1-dev\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo apt install libnotify-dev\n```\n\n#### RedHat Enterprise Linux (RHEL) 8 | RedHat Enterprise Linux (RHEL) 9 | RedHat Enterprise Linux (RHEL) 10\n\n```text\nsudo dnf groupinstall 'Development Tools'\nsudo dnf install libcurl-devel sqlite-devel dbus-devel\ncurl -fsS https://dlang.org/install.sh | bash -s dmd\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo dnf install libnotify-devel\n```\n\n> [!NOTE]\n> **Make sure repos are enabled/subscribed**. Minimal images/containers sometimes don’t have group metadata; on those, the group may appear “not available” until you enable the right repos (or use a full image).\n\n#### Ubuntu 16.x\nUbuntu 16.x LTS reached the end of its five-year LTS window on April 30th 2021 and is no longer supported or tested against.\n\n#### Ubuntu 18.x \nUbuntu 18.x LTS reached the end of its five-year LTS window on May 31th 2023 and is no longer supported or tested against.\n\n#### Ubuntu 20.x\nUbuntu 20.x LTS reached the end of its five-year LTS window on May 31th 2025 and is no longer supported or tested against.\n\n#### Ubuntu 22.x | Ubuntu 24.x\n> [!NOTE]\n> These dependency requirements also apply to any distribution derived from Ubuntu, including but not limited to:\n> *   Lubuntu\n> *   Linux Mint\n> *   Pop!_OS\n> *   Peppermint OS\n> *   Zorin OS\n\n```text\nsudo apt install build-essential\nsudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl systemd-dev libdbus-1-dev\ncurl -fsS https://dlang.org/install.sh | bash -s dmd\n```\nFor GUI notifications the following is also necessary:\n```text\nsudo apt install libnotify-dev\n```\n\n## Clone, Configure, Build, Install\n\n### High Level Steps to building the OneDrive Client for Linux\nThe overall process is as follows:\n1. Install the required platform dependencies (see above)\n2. If necessary, enable your DMD or LDC compiler environment\n3. Clone the GitHub repository\n4. Run the configure script adding any applicable build options (see below), then build the application\n5. Either run the built binary directly from the build directory, or install it system-wide\n6. If applicable, deactivate the DMD or LDC compiler environment when finished\n\n### Building the Application Using Default configure Settings\n\n#### Building on Linux using DMD, LDC or GDC\nYou must first **activate** the compiler environment before building. For example:\n```text\nsource ~/dlang/dmd-2.091.1/activate\n\n# or\n\nsource ~/dlang/ldc-1.20.1/activate\n```\n\nThis command updates your environment (`PATH`, `LIBRARY_PATH`, `LD_LIBRARY_PATH`, etc.) so that the correct compiler is available.\n\nIf you skip this step, the build will fail because the compiler will not be found.\n\n> [!NOTE]\n> Replace the `source` string with the compiler environment activation string displayed when you installed the relevant compiler.\n\nOnce the compiler is activated, clone, build and install the client:\n```text\ngit clone https://github.com/abraunegg/onedrive.git\ncd onedrive\n./configure\nmake clean; make;\nsudo make install\ndeactivate\n```\n\n> [!NOTE]\n> If using GDC ≥ 15, specify it explicitly when configuring the application:\n> ```text\n> ./configure DC=gdc\n> ```\n\n\n#### Building on FreeBSD using gmake\n```text\ngit clone https://github.com/abraunegg/onedrive.git\ncd onedrive\n./configure\ngmake clean; gmake;\ngmake install\n```\n> [!NOTE]\n> Build and install the application as 'root' unless you have installed 'sudo'\n\n#### Building on ARM | Raspberry Pi\n> [!CAUTION]\n> The minimum LDC compiler version required to compile this application is 1.20.1, 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.\n\n> [!IMPORTANT]\n> 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.\n\n> [!NOTE]\n> The `configure` step will detect the correct version of LDC to be used when compiling the client under ARMHF and ARM64 CPU architectures.\n\n```text\ngit clone https://github.com/abraunegg/onedrive.git\ncd onedrive\n./configure; make clean; make;\nsudo make install\n```\n\n### Build Options for Customising the Application\nThe `configure` script provides several options that allow you to tailor the build to your needs. These options can be used to enable or adjust specific features in the client, including:\n* Enabling GUI desktop notifications\n* Enabling shell completion support\n* Enabling internal debugging to assist with troubleshooting and performance analysis\n* Specifying a custom systemd service installation directory\n\n#### Build Option: Enable GUI Desktop Notifications\nTo enable GUI notification support, include the `--enable-notifications` option when running `configure`, for example:\n```text\n./configure --enable-notifications\n```\n\nEnabling this option allows the client to send GUI notifications through the Display Manager via the DBus interface.\n\n> [!TIP]\n> Package maintainers are encouraged to enable this option.\n>\n> When this option is enabled, the client automatically checks at runtime whether GUI notifications can be delivered via the Display Manager through the DBus interface. If this option is **not** enabled, GUI notifications are **disabled**.\n\n#### Build Option: Enable Shell Completion Support\nTo enable command-line shell completions, include the `--enable-completions` option when running `configure`, for example:\n```text\n./configure --enable-completions\n```\nWhen enabled, completion scripts will be installed for **bash**, **zsh**, and **fish** shells.\n\nBy default, the installation directories are detected automatically.\nIf needed, you can manually specify the installation paths using the following options:\n```text\n--with-bash-completion-dir=<DIR>\n--with-zsh-completion-dir=<DIR>\n--with-fish-completion-dir=<DIR>\n```\n\n> [!TIP]\n> Package maintainers are encouraged to enable this option.\n\n#### Build Option: Enabling internal debugging\nTo enable internal debugging support, include the `--enable-debug` option when running `configure`, for example:\n```text\n./configure --enable-debug\n```\nEnabling this option builds the client with additional debug symbols outside of creating a separate debug package build.\n\nThis is particularly useful when investigating performance issues (e.g. with `perf`) or diagnosing application crashes.\n\n**What difference does this make?**\nWithout this option, if the application encounters a crash, the stack trace may contain unresolved symbols, often shown as `??:??`, which makes identifying the cause very difficult.\n\nWith `--enable-debug` enabled, the resulting crash stack trace includes full source file and line information. This allows the issue to be located and isolated quickly and accurately.\n\n> [!TIP]\n> Package maintainers are encouraged to enable this option.\n\n#### Build Option: Customising the Systemd Service Installation Directory\nBy default, systemd service files are installed into the directories detected via `pkg-config --variable=systemdsystemunitdir systemd` and related settings.\n\nIf you need to override these locations, specify one or both of the following options when running `configure`:\n```text\n--with-systemdsystemunitdir=<DIR>   # System-wide service unit directory\n--with-systemduserunitdir=<DIR>     # User-level service unit directory\n```\nTo **disable** installation of a service file entirely, pass `no` as the directory value. For example:\n```text\n./configure --with-systemduserunitdir=no\n```\nThis prevents the corresponding service unit from being installed.\n\n\n## Upgrading the Client\n> [!CAUTION]\n> Before starting any upgrade, **stop any running systemd service for the client**. This ensures the service is restarted using the updated binary.\n\nHow you upgrade depends on how the client was originally installed:\n\n### If installed from a distribution package\nWhen the package maintainer publishes an updated version, the client will be upgraded automatically as part of your normal system package updates (e.g., `apt upgrade`, `dnf upgrade`, `zypper up`, etc.).\n\n### If installed from source\nTo upgrade a source-built installation, the recommended approach is:\n\n1. Uninstall the existing client (see instructions below).\n2. Re-clone the repository.\n3. Re-compile and re-install the new version.\n\n> [!NOTE]\n> The uninstall process removes all components, including systemd service files.\n> If you created custom systemd unit files (e.g., for SharePoint library access), you will need to recreate or restore them after re-installation.\n\nYou **may** choose to skip the uninstall step and simply re-compile and re-install over the top.\nHowever, this risks leaving **multiple** `onedrive` **binaries** on your system.\nDepending on your system `PATH`, the wrong binary may be executed.\n\nAfter installation, verify the version in use:\n```text\nonedrive --version\n```\nThis confirms that the upgrade was successful.\n\n## Uninstalling the client\nHow to uninstall depends on how the client was installed.\n\n### If installed from a distribution package\nUninstall the client using your distribution’s package management tools.\n\nRefer to your distribution’s documentation for the correct removal command (e.g. `apt remove`, `dnf remove`, `zypper remove`, etc.).\n\n### If installed from source\nIf you built and installed the client from a GitHub clone, run the following command from within the cloned repository directory:\n```text\nsudo make uninstall\n```\nThis removes the installed `onedrive` binary and associated system files.\n\n#### Optional: Remove client configuration and state\nIf you do not plan to upgrade or reinstall and wish to remove all client data, run:\n```text\nrm -rf ~/.config/onedrive\n```\n\n> [!IMPORTANT]\n> If you used the `--confdir` option, replace `~/.config/onedrive` with the custom configuration directory you specified.\n\n#### Optional: Remove only the application key\nIf you want to retain your items database but remove the stored authentication token, run:\n```text\nrm -f ~/.config/onedrive/refresh_token\n```\nThis preserves sync state while requiring re-authentication on next run."
  },
  {
    "path": "docs/known-issues.md",
    "content": "# List of Identified Known Issues\nThe following points detail known issues associated with this client:\n\n## Renaming or Moving Files in Standalone Mode causes online deletion and re-upload to occur\n**Issue Tracker:** [#876](https://github.com/abraunegg/onedrive/issues/876), [#2579](https://github.com/abraunegg/onedrive/issues/2579)\n\n**Summary:** \n\nRenaming or moving files and/or folders while using the standalone sync option `--sync` this results in unnecessary data deletion online and subsequent re-upload.\n\n**Detailed Description:**\n\nIn 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.\n\n**Technical Explanation:**\n\nThis 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.\n\n**Recommended Workaround:**\n\nFor 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.\n\n## Application 'stops' running without any visible reason\n**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)\n\n**Summary:**\n\nUsers 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.\n\n**Detailed Description:**\n\nThe 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.\n\nThis problem predominantly arises when the client interacts with Microsoft's Europe Data Centers.\n\n**Technical Explanation:**\n\nThe 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:\n```\nOpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 104\n```\nTo confirm this as the root cause, a detailed HTTPS debug log can be generated with these commands:\n```\n--verbose --verbose --debug-https\n```\n\nThis error typically suggests one of the following issues:\n* An unstable internet connection between the user and the OneDrive service.\n* An issue with HTTPS transparent inspection services that monitor the traffic en route to the OneDrive service.\n\n**Recommended Resolution Steps:**\n\nRecommended steps to address this issue include:\n* Updating your operating system to the latest version.\n* Configure the application to only use HTTP/1.1\n* Configure the application to use IPv4 only.\n* Upgrade your 'curl' application to the latest available from the curl developers.\n* Seeking assistance from your OS vendor.\n* Contacting your Internet Service Provider (ISP) or your IT Help Desk.\n* Reporting the issue to the OpenSSL and/or Curl teams for improved handling of such connection failures.\n* Creating a HTTPS Debug Log during the issue and submitting a support request to Microsoft with the log for their analysis.\n\nFor more in-depth SSL troubleshooting, please read: https://maulwuff.de/research/ssl-debugging.html\n\n\n## AADSTS70000 returned during initial authorisation or re-authentication\n\n**Summary:**\nDuring initial authentication or when running `onedrive --reauth`, the client fails with:\n```\nAADSTS70000: The provided value for the 'code' parameter is not valid\n```\nThis issue is **not a client bug** and is caused by the authorisation code being invalid at the time it is redeemed.\n\n**Detailed Description:**\n\nWhen authenticating, the user is redirected to a Microsoft login page in their web browser. After successful consent, the browser is redirected to a URL of the form:\n```\nhttps://login.microsoftonline.com/common/oauth2/nativeclient?code=<value>\n```\nThe user must copy this URL and paste it back into the CLI when prompted.\n\nMicrosoft authorisation codes are single-use and short-lived. If the code is altered, reused, expired, or otherwise invalidated before the client redeems it, Microsoft Entra ID returns AADSTS70000.\n\n**Technical Explanation:**\n\nThe most common cause is **browser-side interference** with the redirect URL before the user copies it. Privacy and security tooling (such as ad-blockers, URL sanitisation, or “remove tracking parameters” features) can modify or invalidate the `code` query parameter.\n\nOther contributing factors include:\n* Copying the wrong URL (for example, not copying directly from the browser address bar immediately after consent)\n* Refreshing the page or attempting to reuse the same redirect URI\n* Waiting too long before pasting the redirect URI back into the CLI\n\nOnce an authorisation code is invalid, it **cannot** be reused or recovered.\n\n**Recommended Resolution Steps:**\n\n1. Re-run authentication using:\n```\nonedrive --reauth\n```\n2. Use a private/incognito browser session or a clean browser profile\n3. Temporarily disable browser extensions or privacy features that modify URLs for the Microsoft login pages (for example: uBlock Origin, ClearURLs, Brave Shields)\n4. Complete the browser consent flow and immediately copy the redirect URI from the address bar and paste it into the CLI\n\n**Additional Notes:**\n\nFor security reasons, users should **never post full redirect URIs** (they contain sensitive authorisation codes). Any such URLs must be redacted when shared in logs, issues, or support requests."
  },
  {
    "path": "docs/national-cloud-deployments.md",
    "content": "# How to configure access to specific Microsoft Azure deployments\n> [!CAUTION]\n> 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.\n\n## Process Overview\nIn 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.\nCurrent national clouds that are supported are:\n*   Microsoft Cloud for US Government\n*   Microsoft Cloud Germany\n*   Azure and Office365 operated by VNET in China\n\nIn order to successfully use these specific Microsoft Azure deployments, the following steps are required:\n1. Register an application with the Microsoft identity platform using the Azure portal\n2. Configure the new application with the appropriate authentication scopes\n3. Validate that the authentication / redirect URI is correct for your application registration\n4. Configure the onedrive client to use the new application id as provided during application registration\n5. Configure the onedrive client to use the right Microsoft Azure deployment region that your application was registered with\n6. Authenticate the client\n\n## Step 1: Register a new application with Microsoft Azure\n1. Log into your applicable Microsoft Azure Portal with your applicable Office365 identity:\n\n| National Cloud Environment | Microsoft Azure Portal |\n|---|---|\n| Microsoft Cloud for US Government    | https://portal.azure.com/ | \n| Microsoft Cloud Germany              | https://portal.azure.com/ | \n| Azure and Office365 operated by VNET | https://portal.azure.cn/  | \n\n2. Select 'Azure Active Directory' as the service you wish to configure\n3. Under 'Manage', select 'App registrations' to register a new application\n4. Click 'New registration'\n5. Type in the appropriate details required as per below:\n\n![application_registration](./images/application_registration.jpg)\n\n6. To save the application registration, click 'Register' and something similar to the following will be displayed:\n\n![application_registration_done](./images/application_registration_done.jpg)\n\n> [!NOTE]\n> The Application (client) ID UUID as displayed after client registration, is what is required as the 'application_id' for Step 4 below.\n\n## Step 2: Configure application authentication scopes\nConfigure the API permissions as per the following:\n\n| API / Permissions name | Type | Description | Admin consent required |\n|---|---|---|---|\n| Files.ReadWrite | Delegated | Have full access to user files | No |\n| Files.ReadWrite.All  | Delegated | Have full access to all files user can access | No |\n| Sites.ReadWrite.All   | Delegated | Have full access to all items in all site collections | No |\n| offline_access   | Delegated | Maintain access to data you have given it access to | No |\n\n![authentication_scopes](./images/authentication_scopes.jpg)\n\n## Step 3: Validate that the authentication / redirect URI is correct\nAdd the appropriate redirect URI for your Azure deployment:\n\n![authentication_response_uri](./images/authentication_response_uri.jpg)\n\nA valid entry for the response URI should be one of:\n*   https://login.microsoftonline.us/common/oauth2/nativeclient (Microsoft Cloud for US Government)\n*   https://login.microsoftonline.de/common/oauth2/nativeclient (Microsoft Cloud Germany)\n*   https://login.chinacloudapi.cn/common/oauth2/nativeclient (Azure and Office365 operated by VNET in China)\n\nFor a single-tenant application, it may be necessary to use your specific tenant id instead of \"common\":\n*   https://login.microsoftonline.us/example.onmicrosoft.us/oauth2/nativeclient (Microsoft Cloud for US Government)\n*   https://login.microsoftonline.de/example.onmicrosoft.de/oauth2/nativeclient (Microsoft Cloud Germany)\n*   https://login.chinacloudapi.cn/example.onmicrosoft.cn/oauth2/nativeclient (Azure and Office365 operated by VNET in China)\n\n## Step 4: Configure the onedrive client to use new application registration\nUpdate to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following:\n```text\napplication_id = \"insert valid entry here\"\n```\n\nThis will reconfigure the client to use the new application registration you have created.\n\n**Example:**\n```text\napplication_id = \"22c49a0d-d21c-4792-aed1-8f163c982546\"\n```\n\n## Step 5: Configure the onedrive client to use the specific Microsoft Azure deployment\nUpdate to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following:\n```text\nazure_ad_endpoint = \"insert valid entry here\"\n```\n\nValid entries are:\n*   USL4 (Microsoft Cloud for US Government)\n*   USL5 (Microsoft Cloud for US Government - DOD)\n*   DE (Microsoft Cloud Germany)\n*   CN (Azure and Office365 operated by VNET in China)\n\nThis 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)\n\n**Example:**\n```text\nazure_ad_endpoint = \"USL4\"\n```\n\nIf the Microsoft Azure deployment does not support multi-tenant applications, update to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following:\n```text\nazure_tenant_id = \"insert valid entry here\"\n```\n\nThis will configure your client to use the specified tenant id in its Azure AD and Graph endpoint URIs, instead of \"common\".\nThe 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\").\nThe 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.\nThe 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.\n\n**Example:**\n```text\nazure_tenant_id = \"example.onmicrosoft.us\"\n# or\nazure_tenant_id = \"0c4be462-a1ab-499b-99e0-da08ce52a2cc\"\n```\n\n## Step 6: Authenticate the client\nRun the application without any additional command switches.\n\nYou 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.\n```text\n[user@hostname ~]$ onedrive \n\nAuthorize this app visiting:\n\nhttps://.....\n\nEnter the response uri: \n\n```\n\n**Example:**\n```\n[user@hostname ~]$ onedrive \nAuthorize this app visiting:\n\nhttps://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\n\nEnter the response uri: https://login.microsoftonline.com/common/oauth2/nativeclient?code=<redacted>\n\nApplication has been successfully authorised, however no additional command switches were provided.\n\nPlease use --help for further assistance in regards to running this application.\n```\n"
  },
  {
    "path": "docs/podman.md",
    "content": "# Run the OneDrive Client for Linux under Podman\nThis client can be run as a Podman container, with 3 available container base options for you to choose from:\n\n| Container Base | Docker Tag  | Description                                                    | i686 | x86_64 | ARMHF | AARCH64 |\n|----------------|-------------|----------------------------------------------------------------|:------:|:------:|:-----:|:-------:|\n| Alpine Linux   | edge-alpine | Podman container based on Alpine 3.23 using 'master'           |❌|✔|❌|✔|\n| Alpine Linux   | alpine      | Podman container based on Alpine 3.23 using latest release     |❌|✔|❌|✔|\n| Debian         | debian      | Podman container based on Debian 13 using latest release       |✔|✔|✔|✔|\n| Debian         | edge        | Podman container based on Debian 13 using 'master'             |✔|✔|✔|✔|\n| Debian         | edge-debian | Podman container based on Debian 13 using 'master'             |✔|✔|✔|✔|\n| Debian         | latest      | Podman container based on Debian 13 using latest release       |✔|✔|✔|✔|\n| Fedora         | edge-fedora | Podman container based on Fedora 43 using 'master'             |❌|✔|❌|✔|\n| Fedora         | fedora      | Podman container based on Fedora 43 using latest release       |❌|✔|❌|✔|\n\nThese containers offer a simple monitoring-mode service for the OneDrive Client for Linux.\n\nThe instructions below have been validated on:\n*   Fedora 40\n\nThe 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.\n\nThe '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'.\n\nAdditionally 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.\n\n> [!NOTE]\n> The below instructions for podman has been tested and validated when logging into the system as an unprivileged user (non 'root' user).\n\n## High Level Configuration Steps\n1. Install 'podman' as per your distribution platform's instructions if not already installed.\n2. Disable 'SELinux' as per your distribution platform's instructions\n3. Test 'podman' by running a test container\n4. Prepare the required podman volumes to store the configuration and data\n5. Run the 'onedrive' container and perform authorisation\n6. Running the 'onedrive' container under 'podman'\n\n## Configuration Steps\n\n### 1. Install 'podman' on your platform\nInstall 'podman' as per your distribution platform's instructions if not already installed.\n\n### 2. Disable SELinux on your platform\nIn 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:\n```text\nERROR: The local file system returned an error with the following message:\n  Error Message:    /onedrive/conf/refresh_token: Permission denied\n\nThe database cannot be opened. Please check the permissions of ~/.config/onedrive/items.sqlite3\n```\nThe 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.\n\n* Fedora: https://docs.fedoraproject.org/en-US/quick-docs/selinux-changing-states-and-modes/#_disabling_selinux\n* Red Hat Enterprise Linux: https://access.redhat.com/solutions/3176\n\nPost disabling SELinux and reboot your system, confirm that `getenforce` returns `Disabled`:\n```text\n$ getenforce\nDisabled\n```\n\nIf you are still experiencing permission issues despite disabling SELinux, please read https://www.redhat.com/sysadmin/container-permission-denied-errors\n\n### 3. Test 'podman' on your platform\nTest that 'podman' is operational for your 'non-root' user, as per below:\n```bash\n[alex@fedora40-podman ~]$ podman pull fedora\nResolved \"fedora\" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)\nTrying to pull registry.fedoraproject.org/fedora:latest...\nGetting image source signatures\nCopying blob b30887322388 done   | \nCopying config a1cd3cbf8a done   | \nWriting manifest to image destination\na1cd3cbf8adaa422629f2fcdc629fd9297138910a467b11c66e5ddb2c2753dff\n[alex@fedora40-podman ~]$ podman run fedora /bin/echo \"Welcome to the Podman World\"\nWelcome to the Podman World\n[alex@fedora40-podman ~]$ \n```\n\n### 4. Configure the required podman volumes\nThe 'onedrive' Docker container requires 2 podman volumes to operate:\n*    Config Volume\n*    Data Volume\n\nThe 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`\n\nThe 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`\n\n#### 4.1 Prepare the 'config' volume\nCreate the 'config' volume with the following command:\n```bash\npodman volume create onedrive_conf\n```\n\nThis 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.\n\n#### 4.2 Prepare the 'data' volume\nCreate the 'data' volume with the following command:\n```bash\npodman volume create onedrive_data\n```\n\nThis 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:\n\n*   The owner of this specified folder must not be root\n*   Podman will attempt to change the permissions of the volume to the user the container is configured to run as\n\n> [!IMPORTANT]\n> 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:\n> ```bash\n> ROOT level privileges prohibited!\n> ```\n\n### 5. First run of Docker container under podman and performing authorisation\nThe 'onedrive' client within the container first needs to be authorised with your Microsoft account. This is achieved by initially running podman in interactive mode.\n\nRun 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\"`).\n\n> [!IMPORTANT]\n> 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.\n\nIt 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.\n```bash\nexport ONEDRIVE_DATA_DIR=\"${HOME}/OneDrive\"\nexport ONEDRIVE_UID=`id -u`\nexport ONEDRIVE_GID=`id -g`\nmkdir -p ${ONEDRIVE_DATA_DIR}\npodman run -it --name onedrive --user \"${ONEDRIVE_UID}:${ONEDRIVE_GID}\" \\\n    -v onedrive_conf:/onedrive/conf:U,Z \\\n    -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z\" \\\n    driveone/onedrive:edge\n```\n\n> [!IMPORTANT]\n> 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:\n\n```bash\nexport ONEDRIVE_DATA_DIR=\"${HOME}/OneDrive\"\nexport ONEDRIVE_UID=`id -u`\nexport ONEDRIVE_GID=`id -g`\nmkdir -p ${ONEDRIVE_DATA_DIR}\npodman run -it --name onedrive --user \"${ONEDRIVE_UID}:${ONEDRIVE_GID}\" \\\n    --userns=keep-id \\\n    -v onedrive_conf:/onedrive/conf:U,Z \\\n    -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z\" \\\n    driveone/onedrive:edge\n```\n\n\n> [!IMPORTANT]\n> 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:\n\n```bash\nexport ONEDRIVE_DATA_DIR=\"${HOME}/OneDrive\"\nexport ONEDRIVE_UID=`id -u`\nexport ONEDRIVE_GID=`id -g`\nmkdir -p ${ONEDRIVE_DATA_DIR}\npodman run -it --name onedrive --user \"${ONEDRIVE_UID}:${ONEDRIVE_GID}\" \\\n    --userns=keep-id \\\n    -v onedrive_conf:/onedrive/conf:U,Z \\\n    -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z\" \\\n    -e PODMAN=1 \\\n    --label \"io.containers.autoupdate=image\" \\\n    driveone/onedrive:edge\n```\n\nWhen the Podman container successfully starts:\n*   You will be asked to open a specific link using your web browser \n*   Login to your Microsoft Account and give the application the permission \n*   After giving the permission, you will be redirected to a blank page\n*   Copy the URI of the blank page into the application prompt to authorise the application\n\nOnce 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.\n\nIf the client is working as expected, you can detach from the container with Ctrl+p, Ctrl+q.\n\n### 6. Running the 'onedrive' container under 'podman'\n\n#### 6.1 Check if the monitor service is running\n```bash\npodman ps -f name=onedrive\n```\n\n#### 6.2 Show 'onedrive' runtime logs\n```bash\npodman logs onedrive\n```\n\n#### 6.3 Stop running 'onedrive' container\n```bash\npodman stop onedrive\n```\n\n#### 6.4 Start 'onedrive' container\n```bash\npodman start onedrive\n```\n\n#### 6.5 Remove 'onedrive' container\n```bash\npodman rm -f onedrive\n```\n\n## Advanced Usage\n\n### Systemd Service & Auto Updating\n\nPodman 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.\n\n```\ncd /tmp\npodman generate systemd --new --restart-policy on-failure --name -f onedrive\n/tmp/container-onedrive.service\n\n# copy the generated systemd unit file to the systemd path and reload the daemon\n\ncp -Z ~/container-onedrive.service /usr/lib/systemd/system\nsystemctl daemon-reload\n\n#optionally enable it to startup on boot\n\nsystemctl enable container-onedrive.service\n\n#check status\n\nsystemctl status container-onedrive\n\n#start/stop/restart container as a systemd service\n\nsystemctl stop container-onedrive\nsystemctl start container-onedrive\n```\n\nTo update the image using podman (Ad-hoc)\n```\npodman auto-update\n```\n\nTo update the image using systemd (Automatic/Scheduled)\n```\n# Enable the podman-auto-update.timer service at system start:\n\nsystemctl enable podman-auto-update.timer\n\n# Start the service\n\nsystemctl start podman-auto-update.timer\n\n# Containers with the autoupdate label will be updated on the next scheduled timer\n\nsystemctl list-timers --all\n```\n\n### Editing the running configuration and using a 'config' file\nThe '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)  \nThen put it into your onedrive_conf volume path, which can be found with:  \n\n```bash\npodman volume inspect onedrive_conf\n```\nOr 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.\n\nThe 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)\n\n### Syncing multiple accounts\nThere are many ways to do this, the easiest is probably to do the following:\n1. Create a second podman config volume (replace `work` with your desired name):  `podman volume create onedrive_conf_work`\n2. And start a second podman monitor container (again replace `work` with your desired name):\n\n```bash\nexport ONEDRIVE_DATA_DIR_WORK=\"/home/abraunegg/OneDriveWork\"\nexport ONEDRIVE_UID=`id -u`\nexport ONEDRIVE_GID=`id -g`\nmkdir -p ${ONEDRIVE_DATA_DIR_WORK}\npodman run -it --name onedrive_work --user \"${ONEDRIVE_UID}:${ONEDRIVE_GID}\" \\\n    --userns=keep-id \\\n    -v onedrive_conf_work:/onedrive/conf:U,Z \\\n    -v \"${ONEDRIVE_DATA_DIR_WORK}:/onedrive/data:U,Z\" \\\n    -e PODMAN=1 \\\n    --label \"io.containers.autoupdate=image\" \\\n    driveone/onedrive:edge\n```\n\n## Supported Podman Environment Variables\n| Variable | Purpose | Sample Value |\n| ---------------- | --------------------------------------------------- |:-------------:|\n| <B>ONEDRIVE_UID</B> | UserID (UID) to run as  | 1000 |\n| <B>ONEDRIVE_GID</B> | GroupID (GID) to run as | 1000 |\n| <B>ONEDRIVE_VERBOSE</B> | Controls \"--verbose\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_DEBUG</B> | Controls \"--verbose --verbose\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_DEBUG_HTTPS</B> | Controls \"--debug-https\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_RESYNC</B> | Controls \"--resync\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_DOWNLOADONLY</B> | Controls \"--download-only\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_CLEANUPLOCAL</B> | Controls \"--cleanup-local-files\" to cleanup local files and folders if they are removed online. Default is 0 | 1 |\n| <B>ONEDRIVE_UPLOADONLY</B> | Controls \"--upload-only\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_NOREMOTEDELETE</B> | Controls \"--no-remote-delete\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_LOGOUT</B> | Controls \"--logout\" switch. Default is 0 | 1 |\n| <B>ONEDRIVE_REAUTH</B> | Controls \"--reauth\" switch. Default is 0 | 1 |\n| <B>ONEDRIVE_AUTHFILES</B> | Controls \"--auth-files\" option. Default is \"\" | Please read [CLI Option: --auth-files](./application-config-options.md#cli-option---auth-files) |\n| <B>ONEDRIVE_AUTHRESPONSE</B> | Controls \"--auth-response\" option. Default is \"\" | Please read [CLI Option: --auth-response](./application-config-options.md#cli-option---auth-response) |\n| <B>ONEDRIVE_DISPLAY_CONFIG</B> | Controls \"--display-running-config\" switch on onedrive sync. Default is 0 | 1 |\n| <B>ONEDRIVE_SINGLE_DIRECTORY</B> | Controls \"--single-directory\" option. Default = \"\" | \"mydir\" |\n| <B>ONEDRIVE_DRYRUN</B> | Controls \"--dry-run\" option. Default is 0 | 1 |\n| <B>ONEDRIVE_DISABLE_DOWNLOAD_VALIDATION</B> | Controls \"--disable-download-validation\" option. Default is 0 | 1 |\n| <B>ONEDRIVE_DISABLE_UPLOAD_VALIDATION</B> | Controls \"--disable-upload-validation\" option. Default is 0 | 1 |\n| <B>ONEDRIVE_SYNC_SHARED_FILES</B> | Controls \"--sync-shared-files\" option. Default is 0 | 1 |\n| <B>ONEDRIVE_RUNAS_ROOT</B> | Controls if the Docker container should be run as the 'root' user instead of 'onedrive' user. Default is 0 | 1 |\n| <B>ONEDRIVE_SYNC_ONCE</B> | Controls if the Docker container should be run in Standalone Mode. It will use Monitor Mode otherwise. Default is 0 | 1 |\n| <B>ONEDRIVE_FILE_FRAGMENT_SIZE</B> | Controls the fragment size when uploading large files to Microsoft OneDrive. The value specified is in MB. Default is 10, Limit is 60 | 25 |\n| <B>ONEDRIVE_THREADS</B> | Controls the value for the number of worker threads used for parallel upload and download operations. Default is 8, Limit is 16 | 4 |\n\n### Environment Variables Usage Examples\n**Verbose Output:**\n```bash\npodman 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\n```\n**Debug Output:**\n```bash\npodman 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\n```\n**Perform a --resync:**\n```bash\npodman 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\n```\n**Perform a --resync and --verbose:**\n```bash\npodman 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\n```\n**Perform a --logout:**\n```bash\npodman 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\n```\n**Perform a --logout and re-authenticate:**\n```bash\npodman 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\n```\n**Perform a sync using ONEDRIVE_SINGLE_DIRECTORY:**\n```bash\npodman run -e ONEDRIVE_SINGLE_DIRECTORY=\"path/which/needs/to/be/synced\" -v onedrive_conf:/onedrive/conf:U,Z -v \"${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z\" --user \"${ONEDRIVE_UID}:${ONEDRIVE_GID}\" driveone/onedrive:edge\n```\n> [!IMPORTANT]\n> Is using a Podman Environment Variable that requires you to specify a 'path' (ONEDRIVE_AUTHFILES, ONEDRIVE_AUTHRESPONSE, ONEDRIVE_SINGLE_DIRECTORY), the placement of quotes around the path is critically important.\n>\n> Please ensure you are formatting the option correctly:\n>```\n> -e OPTION=\"path/which/needs/to/be/synced\"\n>```\n> Please also ensure that the path specified complies with the actual application usage argument. Please read the relevant config option advice in the [CLI Option Documentation](./application-config-options.md)\n\n\n## Building a custom Podman image\nYou can also build your own image instead of pulling the one from [hub.docker.com](https://hub.docker.com/r/driveone/onedrive):\n```bash\ngit clone https://github.com/abraunegg/onedrive\ncd onedrive\npodman build . -t local-onedrive -f contrib/docker/Dockerfile\n```\n\nThere are alternate, smaller images available by building\nDockerfile-debian or Dockerfile-alpine.  These [multi-stage builder pattern](https://docs.docker.com/develop/develop-images/multistage-build/)\nDockerfiles require Docker version at least 17.05.\n\n### How to build and run a custom Podman image based on Debian\n``` bash\npodman build . -t local-onedrive-debian -f contrib/docker/Dockerfile-debian\npodman 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\n```\n\n### How to build and run a custom Podman image based on Alpine Linux\n``` bash\npodman build . -t local-onedrive-alpine -f contrib/docker/Dockerfile-alpine\npodman 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\n```\n\n### How to build and run a custom Podman image for ARMHF (Raspberry Pi)\nCompatible with:\n*    Raspberry Pi\n*    Raspberry Pi 2\n*    Raspberry Pi Zero\n*    Raspberry Pi 3\n*    Raspberry Pi 4\n``` bash\npodman build . -t local-onedrive-armhf -f contrib/docker/Dockerfile-debian\npodman 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\n```\n\n### How to build and run a custom Podman image for AARCH64 Platforms\n``` bash\npodman build . -t local-onedrive-aarch64 -f contrib/docker/Dockerfile-debian\npodman 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\n```\n"
  },
  {
    "path": "docs/privacy-policy.md",
    "content": "# Privacy Policy\nEffective Date: May 16 2018\n\n## Introduction\n\nThis 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.\n\n## Information We Do Not Collect\n\nWe 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:\n\n1. **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.\n\n2. **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.\n\n3. **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.\n\n## How We Use Your Information\n\nSince we do not collect any personal, usage, or tracking data, there is no information for us to use for any purpose.\n\n## Third-Party Services\n\nThe 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.\n\n## Children's Privacy\n\nSince 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.\n\n## Information You Choose to Share\n\nWhile 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. \n\nWe 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.\n\n## Protecting Your Sensitive Data\n\nWe are committed to safeguarding your sensitive data and maintaining its confidentiality. To ensure its protection:\n\n1. **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.\n\n2. **Data Encryption**: We use industry-standard encryption protocols to protect the transmission and storage of sensitive data.\n\n3. **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.\n\n4. **Security Measures**: We employ robust security measures to prevent unauthorized access, disclosure, or alteration of sensitive data.\n\nBy 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.\n\n## Your Responsibilities\n\nWhile 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.\n\n## Changes to this Privacy Policy\n\nWe 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.\n\n## Contact Us\n\nIf 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)\n\n## Conclusion\n\nBy 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.\n\n"
  },
  {
    "path": "docs/puml/applyPotentiallyChangedItem.puml",
    "content": "@startuml\nstart\npartition \"applyPotentiallyChangedItem\" {\n  :Check if existing item path differs from changed item path;\n  if (itemWasMoved) then (yes)\n    :Log moving item;\n    if (destination exists) then (yes)\n      if (item in database) then (yes)\n        :Check if item is synced;\n        if (item is synced) then (yes)\n          :Log destination is in sync;\n        else (no)\n          :Log destination occupied with a different item;\n          :Backup conflicting file;\n          note right: Local data loss prevention\n        endif\n      else (no)\n        :Log destination occupied by an un-synced file;\n        :Backup conflicting file;\n        note right: Local data loss prevention\n      endif\n    endif\n    :Try to rename path;\n    if (dry run) then (yes)\n      :Track as faked id item;\n      :Track path not renamed;\n    else (no)\n      :Rename item;\n      :Flag item as moved;\n      if (item is a file) then (yes)\n        :Set local timestamp to match online;\n      endif\n    endif\n  else (no)\n  endif\n  :Check if eTag changed;\n  if (eTag changed) then (yes)\n    if (item is a file and not moved) then (yes)\n      :Decide if to download based on hash;\n    else (no)\n      :Update database;\n    endif\n  else (no)\n    :Update database if timestamp differs or in specific operational mode;\n  endif\n}\nstop\n@enduml\n"
  },
  {
    "path": "docs/puml/applyPotentiallyNewLocalItem.puml",
    "content": "@startuml\nstart\npartition \"applyPotentiallyNewLocalItem\" {\n  :Check if path exists;\n  \n  if (Path exists?) then (yes)\n    :Log \"Path on local disk already exists\";\n    \n    if (Is symbolic link?) then (yes)\n      :Log \"Path is a symbolic link\";\n      \n      if (Can read symbolic link?) then (no)\n        :Log \"Reading symbolic link failed\";\n        :Log \"Skipping item - invalid symbolic link\";\n        stop\n      endif\n    endif\n    \n    :Determine if item is in-sync;\n    note right: Execute 'isItemSynced()' function\n    if (Is item in-sync?) then (yes)\n      :Log \"Item in-sync\";\n      :Update/Insert item in DB;\n      stop\n    else (no)\n      :Log \"Item not in-sync\";\n      :Compare local & remote modification times;\n      \n      if (Local time > Remote time?) then (yes)\n        if (ID in database?) then (yes)\n          :Log \"Local file is newer & ID in DB\";\n          :Fetch latest DB record;\n          if (Times equal?) then (yes)\n            :Log \"Times match, keeping local file\";\n          else (no)\n            :Log \"Local time newer, keeping file\";\n            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\n          endif\n          stop\n        else (no)\n          :Log \"Local item not in DB\";\n          if (Bypass data protection?) then (yes)\n            :Log \"WARNING: Data protection disabled\";\n          else (no)\n            :Safe backup local file;\n            note right: Local data loss prevention\n          endif\n          stop\n        endif\n      else (no)\n        if (Remote time > Local time?) then (yes)\n          :Log \"Remote item is newer\";\n          if (Bypass data protection?) then (yes)\n            :Log \"WARNING: Data protection disabled\";\n          else (no)\n            :Safe backup local file;\n            note right: Local data loss prevention\n          endif\n        endif\n        \n        if (Times equal?) then (yes)\n        note left: Specific handling if timestamp was\\nadjusted by isItemSynced()\n          :Log \"Times equal, no action required\";\n          :Update/Insert item in DB;\n          stop\n        endif\n      endif\n    endif\n    \n  else (no)\n    :Handle as potentially new item;\n    switch (Item type) \n    case (File)\n      :Add to download queue;\n    case (Directory)\n      :Log \"Creating local directory\";\n      if (Dry run?) then (no)\n        :Create directory & set attributes;\n        :Save item to DB;\n      else\n        :Log \"Dry run, faking directory creation\";\n        :Save item to dry-run DB;\n      endif\n    case (Unknown)\n      :Log \"Unknown type, no action\";\n    endswitch\n  endif\n}\nstop\n@enduml\n"
  },
  {
    "path": "docs/puml/client_side_filtering_processing_order.puml",
    "content": "@startuml\n|Decision Tree|\n:Start Client Side Filtering Evaluation;\nif (check_nosync?) then (true)\n  :Skip item (no sync);\nelse (false)\n  if (skip_dotfiles?) then (true)\n    :Skip file (dotfile);\n  else (false)\n    if (skip_symlinks?) then (true)\n      :Skip item (symlink);\n    else (false)\n      if (skip_dir?) then (true)\n        :Skip directory;\n      else (false)\n        if (skip_file?) then (true)\n          :Skip file;\n        else (false)\n          if (in sync_list?) then (false)\n            :Skip item (not in sync list);\n          else (true)\n            if (skip_size?) then (true)\n              :Skip file (size too large);\n            else (false)\n              :File or Directory flagged\\nto be synced;\n            endif\n          endif\n        endif\n      endif\n    endif\n  endif\nendif\n:End Client Side Filtering Evaluation;\n@enduml\n"
  },
  {
    "path": "docs/puml/client_side_filtering_rules.puml",
    "content": "@startuml\nstart\n:Start;\npartition \"checkPathAgainstClientSideFiltering\" {\n  :Get localFilePath;\n  \n  if (Does path exist?) then (no)\n    :Return false;\n    stop\n  endif\n  \n  if (Check .nosync?) then (yes)\n    :Check for .nosync file;\n    if (.nosync found) then (yes)\n      :Log and return true;\n      stop\n    endif\n  endif\n  \n  if (Skip dotfiles?) then (yes)\n    :Check if dotfile;\n    if (Is dotfile) then (yes)\n      :Log and return true;\n      stop\n    endif\n  endif\n  \n  if (Skip symlinks?) then (yes)\n    :Check if symlink;\n    if (Is symlink) then (yes)\n      if (Config says skip?) then (yes)\n        :Log and return true;\n        stop\n      elseif (Unexisting symlink?) then (yes)\n        :Check if relative link works;\n        if (Relative link ok) then (no)\n          :Log and return true;\n          stop\n        endif\n      endif\n    endif\n  endif\n  \n  if (Skip dir or file?) then (yes)\n    :Check dir or file exclusion;\n    if (Excluded by config?) then (yes)\n      :Log and return true;\n      stop\n    endif\n  endif\n  \n  if (Use sync_list?) then (yes)\n    :Check sync_list exclusions;\n    if (Excluded by sync_list?) then (yes)\n      :Log and return true;\n      stop\n    endif\n  endif\n  \n  if (Check file size?) then (yes)\n    :Check for file size limit;\n    if (File size exceeds limit?) then (yes)\n      :Log and return true;\n      stop\n    endif\n  endif\n  \n  :Return false;\n}\nstop\n@enduml\n"
  },
  {
    "path": "docs/puml/client_use_of_libcurl.puml",
    "content": "@startuml\nparticipant \"OneDrive Client\\nfor Linux\" as od\nparticipant \"libcurl\" as lc\nparticipant \"Client Web Browser\" as browser\nparticipant \"Microsoft Authentication Service\\n(OAuth 2.0 Endpoint)\" as oauth\nparticipant \"GitHub API\" as github\nparticipant \"Microsoft Graph API\" as graph\n\nactivate od\nactivate lc\n\nod->od: Generate Authentication\\nService URL\nactivate browser\nod->browser: Navigate to Authentication\\nService URL via Client Web Browser\nbrowser->oauth: Request access token\nactivate oauth\noauth-->browser: Access token\nbrowser-->od: Access token\ndeactivate oauth\ndeactivate browser\n\nod->lc: Check application version\\nvia api.github.com\nactivate github\nlc->github: Query release status\nactivate github\ngithub-->lc: Release information\ndeactivate github\nlc-->od: Process release information\ndeactivate lc\n\nloop API Communication\n    od->lc: Construct HTTPS request (with token)\n    activate lc\n    lc->graph: API Request\n    activate graph\n    graph-->lc: API Response\n    deactivate graph\n    lc-->od: Process response\n    deactivate lc\nend\n@enduml\n"
  },
  {
    "path": "docs/puml/code_functional_component_relationships.puml",
    "content": "@startuml\n!define DATABASE_ENTITY(x) entity x\ncomponent main {\n}\ncomponent config {\n}\ncomponent log {\n}\ncomponent curlEngine {\n}\ncomponent util {\n}\ncomponent onedrive {\n}\ncomponent syncEngine {\n}\ncomponent itemdb {\n}\ncomponent clientSideFiltering {\n}\ncomponent monitor {\n}\ncomponent sqlite {\n}\ncomponent qxor {\n}\n\nDATABASE_ENTITY(\"Database\")\n\nmain --> config\nmain --> log\nmain --> curlEngine\nmain --> util\nmain --> onedrive\nmain --> syncEngine\nmain --> itemdb\nmain --> clientSideFiltering\nmain --> monitor\n\nconfig --> log\nconfig --> util\n\nclientSideFiltering --> config\nclientSideFiltering --> util\nclientSideFiltering --> log\n\nsyncEngine --> config\nsyncEngine --> log\nsyncEngine --> util\nsyncEngine --> onedrive\nsyncEngine --> itemdb\nsyncEngine --> clientSideFiltering\n\nutil --> log\nutil --> config\nutil --> qxor\nutil --> curlEngine\n\nsqlite --> log\nsqlite -> \"Database\" : uses\n\nonedrive --> config\nonedrive --> log\nonedrive --> util\nonedrive --> curlEngine\n\nmonitor --> config\nmonitor --> util\nmonitor --> log\nmonitor --> clientSideFiltering\nmonitor .> syncEngine : inotify event\n\nitemdb --> sqlite\nitemdb --> util\nitemdb --> log\n\ncurlEngine --> log\n@enduml\n"
  },
  {
    "path": "docs/puml/conflict_handling_default.puml",
    "content": "@startuml\nstart\nnote left: Operational Mode 'onedrive --sync'\n:Query OneDrive /delta API for online changes;\nnote left: This data is considered the 'source-of-truth'\\nLocal data should be a 'replica' of this data\n:Process received JSON data;\nif (JSON item is a file) then (yes)\n  if (Does the file exist locally) then (yes)\n    :Compute relevant file hashes;\n    :Check DB for file record;\n    if (DB record found) then (yes)\n      :Compare file hash with DB hash;\n      if (Is the hash different) then (yes)\n        :Log that the local file was modified locally since last sync;\n        :Renaming local file to avoid potential local data loss;\n        note left: Local data loss prevention\\nRenamed file will be uploaded as new file\n      else (no)\n      endif\n    else (no)    \n    endif\n  else (no)\n  endif\n:Download file (as per online JSON item) as required;\nelse (no)\n  :Other handling for directories | root objects | deleted items;\nendif\n:Performing a database consistency and\\nintegrity check on locally stored data;\n:Scan file system for any new data to upload;\nnote left: The file that was renamed will be uploaded here\nstop\n@enduml"
  },
  {
    "path": "docs/puml/conflict_handling_default_resync.puml",
    "content": "@startuml\nstart\nnote left: Operational Mode 'onedrive -sync --resync'\n:Query OneDrive /delta API for online changes;\nnote left: This data is considered the 'source-of-truth'\\nLocal data should be a 'replica' of this data\n:Process received JSON data;\nif (JSON item is a file) then (yes)\n  if (Does the file exist locally) then (yes)\n    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\n    if (Can the file be read) then (yes)\n      :Compute UTC timestamp data from local file and JSON data;\n      if (timestamps are equal) then (yes)\n      else (no)\n        :Log that a local file time discrepancy was detected;\n        if (Do file hashes match) then (yes)\n          :Correct the offending timestamp as hashes match;\n        else (no)\n          :Local file is technically different;\n          :Renaming local file to avoid potential local data loss;\n          note left: Local data loss prevention\\nRenamed file will be uploaded as new file\n        endif\n      endif\n    else (no)\n    endif\n  else (no)\n  endif\n:Download file (as per online JSON item) as required;\nelse (no)\n  :Other handling for directories | root objects | deleted items;\nendif\n:Performing a database consistency and\\nintegrity check on locally stored data;\n:Scan file system for any new data to upload;\nnote left: The file that was renamed will be uploaded here\nstop\n@enduml"
  },
  {
    "path": "docs/puml/conflict_handling_local-first_default.puml",
    "content": "@startuml\nstart\nnote left: Operational Mode 'onedrive -sync -local-first'\n:Performing a database consistency and\\nintegrity check on locally stored data;\nnote left: This data is considered the 'source-of-truth'\\nOnline data should be a 'replica' of this data\nrepeat\n  :Process each DB record;\n  if (Is the DB record is in sync with local file) then (yes)\n  \n  else (no)\n  \n    :Log reason for discrepancy;\n    :Flag item to be processed as a modified local file;\n  \n  endif\nrepeat while\n\n:Process modified items to upload;\n\nif (Does local file DB record match current latest online JSON data) then (yes)\n\nelse (no)\n\n  :Log that the local file was modified locally since last sync;\n  :Renaming local file to avoid potential local data loss;\n  note left: Local data loss prevention\\nRenamed file will be uploaded as new file\n  :Upload renamed local file as new file;\n\nendif\n\n:Upload modified file;\n\n:Scan file system for any new data to upload;\n\n:Query OneDrive /delta API for online changes;\n\n:Process received JSON data;\nif (JSON item is a file) then (yes)\n  if (Does the file exist locally) then (yes)\n    :Compute relevant file hashes;\n    :Check DB for file record;\n    if (DB record found) then (yes)\n      :Compare file hash with DB hash;\n      if (Is the hash different) then (yes)\n        :Log that the local file was modified locally since last sync;\n        :Renaming local file to avoid potential local data loss;\n        note left: Local data loss prevention\\nRenamed file will be uploaded as new file\n      else (no)\n      endif\n    else (no)\n    \n    endif\n  else (no)\n  endif\n\n  :Download file (as per online JSON item) as required;\n  \nelse (no)\n  :Other handling for directories | root objects | deleted items;\nendif\nstop\n@enduml"
  },
  {
    "path": "docs/puml/conflict_handling_local-first_resync.puml",
    "content": "@startuml\nstart\nnote left: Operational Mode 'onedrive -sync -local-first -resync'\n:Query OneDrive API and create new database with default root account objects;\n:Performing a database consistency and\\nintegrity check on locally stored data;\nnote left: This data is considered the 'source-of-truth'\\nOnline data should be a 'replica' of this data\\nHowever the database has only 1 record currently\n:Scan file system for any new data to upload;\nnote left: This is where in this specific mode all local\\n content is assessed for applicability for\\nupload to Microsoft OneDrive\n\nrepeat\n  :For each new local item;\n  if (Is the item a directory) then (yes)\n    if (Is Directory found online) then (yes)\n      :Save directory details from online in local database;\n    else (no)\n      :Create directory online;\n      :Save details in local database;\n    endif\n  else (no)\n    :Flag file as a potentially new item to upload;\n  endif\nrepeat while\n\n:Process potential new items to upload;\n\nrepeat\n  :For each potential file to upload;\n  if (Is File found online) then (yes)\n    if (Does the online JSON data match local file) then (yes)\n\t\t  :Save details in local database;\n\t   else (no)\n\t    :Log that the local file was modified locally since last sync;\n\t    :Renaming local file to avoid potential local data loss;\n       note left: Local data loss prevention\\nRenamed file will be uploaded as new file\n       :Upload renamed local file as new file;\n\t  endif\n  else (no)\n    :Upload new file;\n  endif\nrepeat while\n\n:Query OneDrive /delta API for online changes;\n:Process received JSON data;\nif (JSON item is a file) then (yes)\n  if (Does the file exist locally) then (yes)\n    :Compute relevant file hashes;\n    :Check DB for file record;\n    if (DB record found) then (yes)\n      :Compare file hash with DB hash;\n      if (Is the hash different) then (yes)\n        :Log that the local file was modified locally since last sync;\n        :Renaming local file to avoid potential local data loss;\n        note left: Local data loss prevention\\nRenamed file will be uploaded as new file\n      else (no)\n      endif\n    else (no)\n    \n    endif\n  else (no)\n  endif\n\n:Download file (as per online JSON item) as required;\n  \nelse (no)\n  :Other handling for directories | root objects | deleted items;\nendif\n\n\nstop\n@enduml"
  },
  {
    "path": "docs/puml/database_schema.puml",
    "content": "@startuml\n\nclass item {\n    driveId: TEXT\n    id: TEXT\n    name: TEXT\n    remoteName: TEXT\n    type: TEXT\n    eTag: TEXT\n    cTag: TEXT\n    mtime: TEXT\n    parentId: TEXT\n    quickXorHash: TEXT\n    sha256Hash: TEXT\n    remoteDriveId: TEXT\n    remoteParentId: TEXT\n    remoteId: TEXT\n    remoteType: TEXT\n    deltaLink: TEXT\n    syncStatus: TEXT\n    size: TEXT\n    relocDriveId: TEXT\n    relocParentId: TEXT\n}\n\nnote right of item::driveId\n  PRIMARY KEY (driveId, id)\n  FOREIGN KEY (driveId, parentId)\n  REFERENCES item (driveId, id)\n  ON DELETE CASCADE\n  ON UPDATE RESTRICT\nend note\n\nitem --|> item : parentId\n\nnote \"Indexes\" as N1\nnote left of N1\n  name_idx ON item (name)\n  remote_idx ON item (remoteDriveId, remoteId)\n  item_children_idx ON item (driveId, parentId)\n  selectByPath_idx ON item (name, driveId, parentId)\nend note\n\n@enduml\n"
  },
  {
    "path": "docs/puml/default_sync_flow.puml",
    "content": "@startuml\ntitle Default Sync Flow (Online is Source of Truth)\n\nstart\n\n:Step 1 - Scan OneDrive (online);\n:Detect online changes:\n- New files or folders\n- Modified files\n- Deleted files;\n\n:Apply online changes to local:\n- Download new files or folders\n- Update modified files\n- Delete local files or folders;\n\n:Step 2 - Scan local files;\n:Detect local-only changes:\n- New files or folders\n- Modified files;\n\n:Upload local changes to OneDrive:\n- Upload new files or folders\n- Upload modified files;\n\n:Step 3 - Final reconciliation;\n:Rescan OneDrive to ensure:\n- Any last-minute online changes\n  are applied locally;\n\nstop\n@enduml\n"
  },
  {
    "path": "docs/puml/downloadFile.puml",
    "content": "@startuml\nstart\n\npartition \"Download File\" {\n\n  :Get item specifics from JSON;\n  :Calculate item's path;\n  \n  if (Is item malware?) then (yes)\n      :Log malware detected;\n      stop\n  else (no)\n      :Check for file size in JSON;\n      if (File size missing) then (yes)\n          :Log error;\n          stop\n      endif\n      \n      :Configure hashes for comparison;\n      if (Hashes missing) then (yes)\n          :Log error;\n          stop\n      endif\n      \n      if (Does file exist locally?) then (yes)\n          :Check DB for item;\n          if (DB hash match?) then (no)\n              :Log modification; Perform safe backup;\n              note left: Local data loss prevention\n          endif\n      endif\n      \n      :Check local disk space;\n      if (Insufficient space?) then (yes)\n          :Log insufficient space;\n          stop\n      else (no)\n          if (Dry run?) then (yes)\n              :Fake download process;\n          else (no)\n              :Attempt to download file;\n              if (Download exception occurs?) then (yes)\n                  :Handle exceptions; Retry download or log error;\n              endif\n              \n              if (File downloaded successfully?) then (yes)\n                  :Validate download;\n                  if (Validation passes?) then (yes)\n                      :Log success; Update DB;\n                  else (no)\n                      :Log validation failure; Remove file;\n                  endif\n              else (no)\n                  :Log download failed;\n              endif\n          endif\n      endif\n  endif\n\n}\n\nstop\n@enduml\n"
  },
  {
    "path": "docs/puml/high_level_operational_process.puml",
    "content": "@startuml\n\nparticipant \"OneDrive Client\\nfor Linux\" as Client\nparticipant \"Microsoft OneDrive\\nAPI\" as API\n\n== Access Token Validation ==\nClient -> Client: Validate access and\\nexisting access token\\nRefresh if needed\n\n== Query Microsoft OneDrive /delta API ==\nClient -> API: Query /delta API\nAPI -> Client: JSON responses\n\n== Process JSON Responses ==\nloop for each JSON response\n    Client -> Client: Determine if JSON is 'root'\\nor 'deleted' item\\nElse, push into temporary array for further processing\n    alt if 'root' or 'deleted'\n        Client -> Client: Process 'root' or 'deleted' items\n    else\n        Client -> Client: Evaluate against 'Client Side Filtering' rules\n        alt if unwanted\n            Client -> Client: Discard JSON\n        else\n            Client -> Client: Process JSON (create dir/download file)\n            Client -> Client: Save in local database cache\n        end\n    end\nend\n\n== Local Cache Database Processing for Data Integrity ==\nClient -> Client: Process local cache database\\nto check local data integrity and for differences\nalt if difference found\n    Client -> API: Upload file/folder change including deletion\n    API -> Client: Response with item metadata\n    Client -> Client: Save response to local cache database\nend\n\n== Local Filesystem Scanning ==\nClient -> Client: Scan local filesystem\\nfor new files/folders\n\nloop for each new item\n    Client -> Client: Check item against 'Client Side Filtering' rules\n    alt if item passes filtering\n        Client -> API: Upload new file/folder change including deletion\n        API -> Client: Response with item metadata\n        Client -> Client: Save response in local\\ncache database\n    else\n        Client -> Client: Discard item\\n(Does not meet filtering criteria)\n    end\nend\n\n== Final Data True-Up ==\nClient -> API: Query /delta link for true-up\nAPI -> Client: Process further online JSON changes if required\n\n@enduml\n"
  },
  {
    "path": "docs/puml/is_item_in_sync.puml",
    "content": "@startuml\nstart\npartition \"Is item in sync\" {\n  :Check if path exists;\n  if (path does not exist) then (no)\n    :Return false;\n    stop\n  else (yes)\n  endif\n  \n  :Identify item type;\n  switch (item type)\n  case (file)\n  \n    :Check if path is a file;\n    if (path is not a file) then (no)\n      :Log \"item is a directory but should be a file\";\n      :Return false;\n      stop\n    else (yes)\n    endif\n    \n    :Attempt to read local file;\n    if (file is unreadable) then (no)\n      :Log \"file cannot be read\";\n      :Return false;\n      stop\n    else (yes)\n    endif\n    \n    :Get local and input item modified time;\n    note right: The 'input item' could be a database reference object, or the online JSON object\\nas provided by the Microsoft OneDrive API\n    :Reduce time resolution to seconds;\n    \n    if (localModifiedTime == itemModifiedTime) then (yes)\n      :Return true;\n      stop\n    else (no)\n      :Log time discrepancy;\n    endif\n    \n    :Check if file hash is the same;\n    if (hash is the same) then (yes)\n      :Log \"hash match, correcting timestamp\";\n      if (local time > item time) then (yes)\n        if (download only mode) then (no)\n          :Correct timestamp online if not dryRun;\n        else (yes)\n          :Correct local timestamp if not dryRun;\n        endif\n      else (no)\n        :Correct local timestamp if not dryRun;\n      endif\n      :Return false;\n      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.\n      stop\n    else (no)\n      :Log \"different hash\";\n      :Return false;\n      stop\n    endif\n  \n  case (dir or remote)\n    :Check if path is a directory;\n    if (path is a directory) then (yes)\n      :Return true;\n      stop\n    else (no)\n      :Log \"item is a file but should be a directory\";\n      :Return false;\n      stop\n    endif\n  \n  case (unknown)\n    :Return true but do not sync;\n    stop\n  endswitch\n}\n@enduml\n"
  },
  {
    "path": "docs/puml/local_first_sync_process.puml",
    "content": "@startuml\ntitle Local-First Sync Flow (--local-first)\n\nstart\n\n:Step 1 - Scan local files;\n:Detect local changes:\n- New files or folders\n- Modified files\n- Deleted files;\n\n:Apply local changes to OneDrive:\n- Upload new files or folders\n- Update modified files\n- Delete files or folders on OneDrive;\n\n:Step 2 - Scan OneDrive (online);\n:Detect online-only changes:\n- New files or folders\n- Modified files\n- Deleted files;\n\n:Apply online changes to local:\n- Download missing files or folders\n- Update outdated local files\n- Delete local files or folders\n  that were deleted online;\n\nstop\n@enduml\n"
  },
  {
    "path": "docs/puml/main_activity_flows.puml",
    "content": "@startuml\n\nstart\n\n:Validate access and existing access token\\nRefresh if needed;\n\n:Query /delta API;\nnote right: Query Microsoft OneDrive /delta API\n:Receive JSON responses;\n\n:Process JSON Responses;\npartition \"Process /delta JSON Responses\" {\n    while (for each JSON response) is (yes)\n        :Determine if JSON is 'root'\\nor 'deleted' item;\n        if ('root' or 'deleted') then (yes)\n            :Process 'root' or 'deleted' items;\n            if ('root' object) then (yes)\n                :Process 'root' JSON;\n            else (no)\n                if (Is 'deleted' object in sync) then (yes)\n                    :Process deletion of local item;\n                else (no)\n                    :Rename local file as it is not in sync;\n                    note right: Deletion event conflict handling\\nLocal data loss prevention\n                endif\n            endif\n        else (no)\n            :Evaluate against 'Client Side Filtering' rules;\n            if (unwanted) then (yes)\n                :Discard JSON;\n            else (no)\n                :Process JSON (create dir/download file);\n                if (Is the 'JSON' item in the local cache) then (yes)\n                  :Process JSON as a potentially changed local item;\n                  note left: Run 'applyPotentiallyChangedItem' function\n                else (no)\n                  :Process JSON as potentially new local item;\n                  note right:  Run 'applyPotentiallyNewLocalItem' function\n                endif\n                :Process objects in download queue;\n                :Download File;\n                note left: Download file from Microsoft OneDrive (Multi Threaded Download)\n                :Save in local database cache;\n            endif\n        endif\n    endwhile\n}\n\npartition \"Perform data integrity check based on local cache database\" {\n  :Process local cache database\\nto check local data integrity and for differences;\n  if (difference found) then (yes)\n      :Upload file/folder change including deletion;\n      note right: Upload local change to Microsoft OneDrive\n      :Receive response with item metadata;\n      :Save response to local cache database;\n  else (no)\n  endif\n}\n\npartition \"Local Filesystem Scanning\" {\n  :Scan local filesystem\\nfor new files/folders;\n    while (for each new item) is (yes)\n        :Check item against 'Client Side Filtering' rules;\n        if (item passes filtering) then (yes)\n            :Upload new file/folder change including deletion;\n            note right: Upload to Microsoft OneDrive\n            :Receive response with item metadata;\n            :Save response in local\\ncache database;\n        else (no)\n            :Discard item\\n(Does not meet filtering criteria);\n        endif\n    endwhile\n}\n\npartition \"Final True-Up\" {\n  :Query /delta link for true-up;\n  note right: Final Data True-Up\n  :Process further online JSON changes if required;\n}\nstop\n@enduml"
  },
  {
    "path": "docs/puml/onedrive_linux_authentication.puml",
    "content": "@startuml\nparticipant \"OneDrive Client for Linux\"\nparticipant \"Microsoft OneDrive\\nAuthentication Service\\n(login.microsoftonline.com)\" as AuthServer\nparticipant \"User's Device (for MFA)\" as UserDevice\nparticipant \"Microsoft Graph API\\n(graph.microsoft.com)\" as GraphAPI\nparticipant \"Microsoft OneDrive\"\n\n\"OneDrive Client for Linux\" -> AuthServer: Request Authorization\\n(Client Credentials, Scopes)\nAuthServer -> \"OneDrive Client for Linux\": Provide Authorization Code\n\n\"OneDrive Client for Linux\" -> AuthServer: Request Access Token\\n(Authorization Code, Client Credentials)\n\nalt MFA Enabled\n    AuthServer -> UserDevice: Trigger MFA Challenge\n    UserDevice -> AuthServer: Provide MFA Verification\n    AuthServer -> \"OneDrive Client for Linux\": Return Access Token\\n(and Refresh Token)\n    \"OneDrive Client for Linux\" -> GraphAPI: Request Microsoft OneDrive Data\\n(Access Token)\n    loop Token Expiry Check\n        \"OneDrive Client for Linux\" -> AuthServer: Is Access Token Expired?\n        alt Token Expired\n            \"OneDrive Client for Linux\" -> AuthServer: Request New Access Token\\n(Refresh Token)\n            AuthServer -> \"OneDrive Client for Linux\": Return New Access Token\n        else Token Valid\n            GraphAPI -> \"Microsoft OneDrive\": Retrieve Data\n            \"Microsoft OneDrive\" -> GraphAPI: Return Data\n            GraphAPI -> \"OneDrive Client for Linux\": Provide Data\n        end\n    end\nelse MFA Not Required\n    AuthServer -> \"OneDrive Client for Linux\": Return Access Token\\n(and Refresh Token)\n    \"OneDrive Client for Linux\" -> GraphAPI: Request Microsoft OneDrive Data\\n(Access Token)\n    loop Token Expiry Check\n        \"OneDrive Client for Linux\" -> AuthServer: Is Access Token Expired?\n        alt Token Expired\n            \"OneDrive Client for Linux\" -> AuthServer: Request New Access Token\\n(Refresh Token)\n            AuthServer -> \"OneDrive Client for Linux\": Return New Access Token\n        else Token Valid\n            GraphAPI -> \"Microsoft OneDrive\": Retrieve Data\n            \"Microsoft OneDrive\" -> GraphAPI: Return Data\n            GraphAPI -> \"OneDrive Client for Linux\": Provide Data\n        end\n    end\nelse MFA Failed or Other Auth Error\n    AuthServer -> \"OneDrive Client for Linux\": Error Message (e.g., Invalid Credentials, MFA Failure)\nend\n\n@enduml"
  },
  {
    "path": "docs/puml/onedrive_windows_ad_authentication.puml",
    "content": "@startuml\nparticipant \"Microsoft Windows OneDrive Client\"\nparticipant \"Azure Active Directory\\n(Active Directory)\\n(login.microsoftonline.com)\" as AzureAD\nparticipant \"Microsoft OneDrive\\nAuthentication Service\\n(login.microsoftonline.com)\" as AuthServer\nparticipant \"User's Device (for MFA)\" as UserDevice\nparticipant \"Microsoft Graph API\\n(graph.microsoft.com)\" as GraphAPI\nparticipant \"Microsoft OneDrive\"\n\n\"Microsoft Windows OneDrive Client\" -> AzureAD: Request Authorization\\n(Client Credentials, Scopes)\nAzureAD -> AuthServer: Validate Credentials\\n(Forward Request)\nAuthServer -> AzureAD: Provide Authorization Code\nAzureAD -> \"Microsoft Windows OneDrive Client\": Provide Authorization Code (via AzureAD)\n\n\"Microsoft Windows OneDrive Client\" -> AzureAD: Request Access Token\\n(Authorization Code, Client Credentials)\nAzureAD -> AuthServer: Request Access Token\\n(Authorization Code, Forwarded Credentials)\nAuthServer -> AzureAD: Return Access Token\\n(and Refresh Token)\nAzureAD -> \"Microsoft Windows OneDrive Client\": Return Access Token\\n(and Refresh Token) (via AzureAD)\n\nalt MFA Enabled\n    AzureAD -> UserDevice: Trigger MFA Challenge\n    UserDevice -> AzureAD: Provide MFA Verification\n    AzureAD -> \"Microsoft Windows OneDrive Client\": Return Access Token\\n(and Refresh Token) (Post MFA)\n    \"Microsoft Windows OneDrive Client\" -> GraphAPI: Request Microsoft OneDrive Data\\n(Access Token)\n    loop Token Expiry Check\n        \"Microsoft Windows OneDrive Client\" -> AzureAD: Is Access Token Expired?\n        AzureAD -> AuthServer: Validate Token Expiry\n        alt Token Expired\n            \"Microsoft Windows OneDrive Client\" -> AzureAD: Request New Access Token\\n(Refresh Token)\n            AzureAD -> AuthServer: Request New Access Token\\n(Refresh Token)\n            AuthServer -> AzureAD: Return New Access Token\n            AzureAD -> \"Microsoft Windows OneDrive Client\": Return New Access Token (via AzureAD)\n        else Token Valid\n            GraphAPI -> \"Microsoft OneDrive\": Retrieve Data\n            \"Microsoft OneDrive\" -> GraphAPI: Return Data\n            GraphAPI -> \"Microsoft Windows OneDrive Client\": Provide Data\n        end\n    end\nelse MFA Not Required\n    AzureAD -> \"Microsoft Windows OneDrive Client\": Return Access Token\\n(and Refresh Token) (Direct)\n    \"Microsoft Windows OneDrive Client\" -> GraphAPI: Request Microsoft OneDrive Data\\n(Access Token)\n    loop Token Expiry Check\n        \"Microsoft Windows OneDrive Client\" -> AzureAD: Is Access Token Expired?\n        AzureAD -> AuthServer: Validate Token Expiry\n        alt Token Expired\n            \"Microsoft Windows OneDrive Client\" -> AzureAD: Request New Access Token\\n(Refresh Token)\n            AzureAD -> AuthServer: Request New Access Token\\n(Refresh Token)\n            AuthServer -> AzureAD: Return New Access Token\n            AzureAD -> \"Microsoft Windows OneDrive Client\": Return New Access Token (via AzureAD)\n        else Token Valid\n            GraphAPI -> \"Microsoft OneDrive\": Retrieve Data\n            \"Microsoft OneDrive\" -> GraphAPI: Return Data\n            GraphAPI -> \"Microsoft Windows OneDrive Client\": Provide Data\n        end\n    end\nelse MFA Failed or Other Auth Error\n    AzureAD -> \"Microsoft Windows OneDrive Client\": Error Message (e.g., Invalid Credentials, MFA Failure)\nend\n\n@enduml\n"
  },
  {
    "path": "docs/puml/onedrive_windows_authentication.puml",
    "content": "@startuml\nparticipant \"Microsoft Windows OneDrive Client\"\nparticipant \"Microsoft OneDrive\\nAuthentication Service\\n(login.microsoftonline.com)\" as AuthServer\nparticipant \"User's Device (for MFA)\" as UserDevice\nparticipant \"Microsoft Graph API\\n(graph.microsoft.com)\" as GraphAPI\nparticipant \"Microsoft OneDrive\"\n\n\"Microsoft Windows OneDrive Client\" -> AuthServer: Request Authorization\\n(Client Credentials, Scopes)\nAuthServer -> \"Microsoft Windows OneDrive Client\": Provide Authorization Code\n\n\"Microsoft Windows OneDrive Client\" -> AuthServer: Request Access Token\\n(Authorization Code, Client Credentials)\n\nalt MFA Enabled\n    AuthServer -> UserDevice: Trigger MFA Challenge\n    UserDevice -> AuthServer: Provide MFA Verification\n    AuthServer -> \"Microsoft Windows OneDrive Client\": Return Access Token\\n(and Refresh Token)\n    \"Microsoft Windows OneDrive Client\" -> GraphAPI: Request Microsoft OneDrive Data\\n(Access Token)\n    loop Token Expiry Check\n        \"Microsoft Windows OneDrive Client\" -> AuthServer: Is Access Token Expired?\n        alt Token Expired\n            \"Microsoft Windows OneDrive Client\" -> AuthServer: Request New Access Token\\n(Refresh Token)\n            AuthServer -> \"Microsoft Windows OneDrive Client\": Return New Access Token\n        else Token Valid\n            GraphAPI -> \"Microsoft OneDrive\": Retrieve Data\n            \"Microsoft OneDrive\" -> GraphAPI: Return Data\n            GraphAPI -> \"Microsoft Windows OneDrive Client\": Provide Data\n        end\n    end\nelse MFA Not Required\n    AuthServer -> \"Microsoft Windows OneDrive Client\": Return Access Token\\n(and Refresh Token)\n    \"Microsoft Windows OneDrive Client\" -> GraphAPI: Request Microsoft OneDrive Data\\n(Access Token)\n    loop Token Expiry Check\n        \"Microsoft Windows OneDrive Client\" -> AuthServer: Is Access Token Expired?\n        alt Token Expired\n            \"Microsoft Windows OneDrive Client\" -> AuthServer: Request New Access Token\\n(Refresh Token)\n            AuthServer -> \"Microsoft Windows OneDrive Client\": Return New Access Token\n        else Token Valid\n            GraphAPI -> \"Microsoft OneDrive\": Retrieve Data\n            \"Microsoft OneDrive\" -> GraphAPI: Return Data\n            GraphAPI -> \"Microsoft Windows OneDrive Client\": Provide Data\n        end\n    end\nelse MFA Failed or Other Auth Error\n    AuthServer -> \"Microsoft Windows OneDrive Client\": Error Message (e.g., Invalid Credentials, MFA Failure)\nend\n\n@enduml"
  },
  {
    "path": "docs/puml/uploadFile.puml",
    "content": "@startuml\nstart\npartition \"Upload File\" {\n  :Log \"fileToUpload\";\n  :Check database for parent path;\n  if (parent path found?) then (yes)\n      if (drive ID not empty?) then (yes)\n          :Proceed;\n      else (no)\n          :Use defaultDriveId;\n      endif\n  else (no)\n      stop\n  endif\n  :Check if file exists locally;\n  if (file exists?) then (yes)\n      :Read local file;\n      if (can read file?) then (yes)\n          if (parent path in DB?) then (yes)\n              :Get file size;\n              if (file size <= max?) then (yes)\n                  :Check available space on OneDrive;\n                  if (space available?) then (yes)\n                      :Check if file exists on OneDrive;\n                      if (file exists online?) then (yes)\n                          :Save online metadata only;\n                          if (if local file newer) then (yes)\n                            :Local file is newer;\n                            :Upload file as changed local file;\n                          else (no)\n                            :Remote file is newer;\n                            :Perform safe backup;\n                            note right: Local data loss prevention\n                            :Upload renamed file as new file;\n                          endif\n                      else (no)\n                          :Attempt upload;\n                      endif\n                  else (no)\n                      :Log \"Insufficient space\";\n                  endif\n              else (no)\n                  :Log \"File too large\";\n              endif\n          else (no)\n              :Log \"Parent path issue\";\n          endif\n      else (no)\n          :Log \"Cannot read file\";\n      endif\n  else (no)\n      :Log \"File disappeared locally\";\n  endif\n  :Upload success or failure;\n  if (upload failed?) then (yes)\n      :Log failure;\n  else (no)\n      :Update cache;\n  endif\n}  \nstop\n@enduml\n"
  },
  {
    "path": "docs/puml/uploadModifiedFile.puml",
    "content": "@startuml\nstart\npartition \"Upload Modified File\" {\n  :Initialize API Instance;\n  :Check for Dry Run;\n  if (Is Dry Run?) then (yes)\n    :Create Fake Response;\n  else (no)\n    :Get Current Online Data;\n    if (Error Fetching Data) then (yes)\n      :Handle Errors;\n      if (Retryable Error?) then (yes)\n        :Retry Fetching Data;\n        detach\n      else (no)\n        :Log and Display Error;\n      endif\n    endif\n    if (filesize > 0 and valid latest online data) then (yes)\n      if (is online file newer) then (yes)\n        :Log that online is newer; \n        :Perform safe backup;\n        note left: Local data loss prevention\n        :Upload renamed local file as new file;\n      endif\n    endif\n    :Determine Upload Method;\n    if (Use Simple Upload?) then (yes)\n      :Perform Simple Upload;\n      if (Upload Error) then (yes)\n        :Handle Upload Errors and Retries;\n        if (Retryable Upload Error?) then (yes)\n          :Retry Upload;\n          detach\n        else (no)\n          :Log and Display Upload Error;\n        endif\n      endif\n    else (no)\n      :Create Upload Session;\n      :Perform Upload via Session;\n      if (Session Upload Error) then (yes)\n        :Handle Session Upload Errors and Retries;\n        if (Retryable Session Error?) then (yes)\n          :Retry Session Upload;\n          detach\n        else (no)\n          :Log and Display Session Error;\n        endif\n      endif\n    endif\n  endif\n  :Finalize;\n}\nstop\n@enduml\n"
  },
  {
    "path": "docs/puml/webhooks.puml",
    "content": "@startuml\n\nskinparam SequenceBoxBackgroundColor<<Internal>> AliceBlue\n\nbox \"Linux System\"<<Internal>>\n  participant ClientApp as \"OneDrive Client for Linux\\n(webhook listener 127.0.0.1:8888)\"\n  participant Nginx\nend box\n\nparticipant Firewall as \"Firewall | Router\"\nparticipant GraphAPI as \"Microsoft Graph API\"\n\nClientApp -> GraphAPI: HTTPS POST /v1.0/subscriptions\nGraphAPI -> ClientApp: Subscription details response (HTTPS)\n\n== Subscription Notification ==\nGraphAPI -> Firewall: HTTPS Notification (port 443)\nFirewall -> Nginx: Port forwarding to Nginx (port 443)\n\nalt Request for /webhooks/onedrive\n    Nginx -> ClientApp: Proxy notification to http://127.0.0.1:8888\n    ClientApp -> Nginx: Response\n    Nginx -> GraphAPI: Return proxied response (HTTPS)\nend\n@enduml"
  },
  {
    "path": "docs/server-side-filtering-limitations.md",
    "content": "# Why 'Server Side Filtering' is not possible with Microsoft OneDrive\n\nA common misconception is that `sync_list` or other client-side filtering rules should be able to instruct Microsoft OneDrive or Microsoft Graph to only return a subset of data from the server.\n\nThis is not how Microsoft OneDrive or Microsoft Graph works.\n\nThe Microsoft Graph API exposes OneDrive content as `driveItem` resources. Folders are represented as items with a `children` relationship, and changes are tracked through the `delta` API. In other words, the API is built around addressing items, listing children, and tracking changes to those items over time. It is **not** built around applying a user-defined selective sync policy on the server before results are returned.\n\n## The practical reality\n\nServer-side selective sync, equivalent to `sync_list`, is not possible with Microsoft Graph today. There is no supported API capability to provide Microsoft Graph with rules such as:\n\n* include these folders\n* exclude these folders\n* exclude this subtree recursively\n* apply wildcard or glob rules\n* return only the logical drive view that matches a client configuration\n\nThe OneDrive Client for Linux therefore has no ability to tell Microsoft Graph:\n\n> only return `/Documents/Work/**`, but exclude `/Documents/Work/Archive/**`\n\nThat type of policy-driven filesystem view is simply not part of the API surface exposed by Microsoft Graph.\n\n## Why this is a Microsoft Graph platform limitation\n\nThis is not an implementation gap in the OneDrive Client for Linux. It is a direct result of how Microsoft Graph is designed.\n\nThe `children` API for drive items supports paging and response-shaping options such as `$expand`, `$select`, `$skipToken`, `$top`, and `$orderby`, but it does **not** support a hierarchical `$filter` capability that could be used to express selective sync rules. Microsoft’s own query parameter guidance also states that support for query parameters varies by API operation, and the supported parameters for each operation are explicitly documented. For `children`, the supported query parameters do not include the type of recursive or path-based filtering that `sync_list` would require.\n\n### Why `$filter` does not solve this\n\nEven where Microsoft Graph supports `$filter` on other APIs, that does not make server-side selective sync possible for OneDrive content. Selective sync requires the server to understand and evaluate:\n\n* full path ancestry\n* descendant relationships\n* recursive subtree inclusion and exclusion\n* ordered rule processing\n* wildcard or glob matching\n* conflict handling between include and exclude rules\n\nThe OneDrive `children` API does not expose that model. It returns the items in a folder. The client must then decide what those returned items mean in the context of the configured client-side sync rules.\n\n### Why `search` does not solve this\n\nIt may be tempting to think that the Graph search API could be used instead.\n\nIt cannot.\n\nThe Graph search endpoint is a search function over drive content using query text. It is designed to find matching items by search criteria such as filename, metadata, or file content. It is **not** a policy engine, it is not a substitute for authoritative filesystem enumeration, and it cannot be used to enforce deterministic include/exclude boundaries for sync.\n\nSearch can help find items. It cannot define a complete and correct sync scope.\n\n### Why `delta` does not solve this\n\nThe Graph `delta` API is also often misunderstood.\n\n`delta` is designed to track changes in a `driveItem` and its children over time. Microsoft documents that the app begins by calling `delta` with no parameters, and that the service starts **enumerating the drive's hierarchy**, returning pages of items until the client has received the complete change set. After that, the client applies those changes to its local state.\n\nThis is important:\n\n* `delta` reduces how much metadata needs to be transferred after the initial state is known\n* `delta` helps the client track change efficiently\n* `delta` does **not** move selective sync rule evaluation to the server\n* `delta` still assumes the client is responsible for deciding what to keep, ignore, download, or discard locally\n\n## What the client must do instead\n\nBecause Microsoft Graph does not provide server-side selective sync, the OneDrive Client for Linux must do the following:\n\n1. Enumerate remote metadata from Microsoft OneDrive\n2. Build or refresh its understanding of the remote hierarchy\n3. Evaluate configured rules such as `sync_list`, `skip_file`, `skip_dir`, `single_directory`, and other sync controls\n4. Decide locally which items should be downloaded, ignored, retained, or removed\n\nThis is why `sync_list` and other sync controls are correctly described as client side filtering.\n\nThe rules are applied by the client after Microsoft Graph has returned the relevant metadata required for the client to understand the remote state.\n\n## Why excluded data may still appear to be “seen”\n\nUsers sometimes ask:\n\n> If I’ve excluded most folders using `sync_list`, why does the client still appear to scan the entire remote structure before skipping them?\n\nThe answer is simple:\n\nTo decide whether something should be excluded, the client must first know that the item exists in the remote hierarchy. Microsoft Graph returns metadata about drive items and folder children; the client then applies its local filtering rules to determine whether that item should be processed further.\n\nSo:\n\n* the client may enumerate metadata for excluded paths\n* the client may log that those paths were evaluated\n* the client may discard them immediately based on local rules\n* the client is **not** “pulling everything down” in the sense of downloading all file content. \n\nWhat is unavoidable is remote metadata discovery. What is controlled by client-side filtering is what happens after that discovery process.\n\n## Why “only query allowed folders” is not a complete solution\n\nAnother suggestion is often:\n\n> Why not just query only the folders I want?\n\nThat approach is incomplete and unreliable. A sync client must correctly handle:\n\n* new folders created remotely\n* renames and moves\n* deleted items\n* items relocated into or out of an allowed path\n* invalidated delta tokens\n* reconciliation of local and remote state across the full hierarchy\n\nWithout authoritative knowledge of the hierarchy and changes returned by Microsoft Graph, the client cannot safely and correctly maintain sync state. The Graph API is designed around item enumeration and delta tracking, not around returning a server-enforced filtered filesystem view.\n\n## What this means for all Microsoft OneDrive clients\n\nThis limitation is not unique to the OneDrive Client for Linux.\n\nAny OneDrive client built on Microsoft Graph must work within the same platform constraints:\n\n* Microsoft Graph returns OneDrive content as addressable resources and collections of `driveItem` objects\n* folder traversal happens through `children`\n* change tracking happens through `delta`\n* filtering decisions (if implemented) beyond what the API explicitly supports must be made by the client\n\n## Summary\n\nServer-side selective sync is not available because Microsoft Graph does not provide:\n\n* recursive path-based filtering\n* wildcard rule evaluation\n* hierarchical include/exclude policy support\n* a server-defined partial-drive view for sync clients\n\nAs a result, the client must always enumerate the remote OneDrive metadata to understand the full filesystem structure before any filtering rules can be applied locally.\n\nThis enumeration phase can take a noticeable amount of time on large datasets (for example, SharePoint libraries with tens of thousands of folders). This is especially evident when using `--resync`, which clears all locally stored sync state and forces a full re-discovery of the remote hierarchy, or when changes to configuration (such as `sync_list`) require the client to re-evaluate the complete remote structure.\n\nIt is important to understand that this process is **metadata enumeration only** — the client is not downloading all file contents, but it must still query and process all relevant filesystem objects returned by Microsoft Graph.\n\nAdditionally, this process cannot be arbitrarily parallelised or short-circuited. Microsoft Graph returns data in a paginated and ordered manner, and the client must process these results sequentially to correctly maintain state, handle hierarchy relationships, and ensure consistency (for example, detecting moves, renames, and deletions). Attempting to process this out of order or in parallel would lead to an inconsistent or incorrect sync state.\n\nThis means that:\n\n* initial syncs and `--resync` operations will take longer on large datasets\n* applying or modifying filtering rules may require full re-evaluation\n* large numbers of folders or items will increase enumeration time\n\nThis behaviour is therefore **expected**, **correct**, and **driven by Microsoft Graph platform limitations**, not by a defect in the OneDrive Client for Linux.\n\n\n\n"
  },
  {
    "path": "docs/sharepoint-libraries.md",
    "content": "# How to configure OneDrive SharePoint Shared Library sync\n\n> [!CAUTION]\n> 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.\n\n> [!CAUTION]\n> 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.\n>\n> When this has been investigated, the following has been noted as potential root causes:\n> *  File indexing application such as Baloo File Indexer or Tracker3 constantly indexing your OneDrive data\n> *  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.\n> \n> 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.\n> \n> **Possible Preventative Actions:**\n> *  Disable all File Indexing for your SharePoint Library data. It is out of scope to detail on how you should do this.\n> *  Disable using a systemd service for syncing your SharePoint Library data.\n> *  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.\n> \n> 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.\n\n## Process Overview\nSyncing a OneDrive SharePoint library requires additional configuration for your 'onedrive' client:\n1.  Login to OneDrive and under 'Shared Libraries' obtain the shared library name\n2.  Query that shared library name using the client to obtain the required configuration details\n3.  Create a unique local folder which will be the SharePoint Library 'root'\n4.  Configure the client's config file with the required 'drive_id'\n5.  Test the configuration using '--dry-run'\n6.  Sync the SharePoint Library as required\n\n> [!IMPORTANT]\n> 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.\n\n## 1. Listing available OneDrive SharePoint Libraries\nLogin to the OneDrive web interface and determine which shared library you wish to configure the client for:\n![shared_libraries](./images/SharedLibraries.jpg)\n\n## 2. Query OneDrive API to obtain required configuration details\nRun 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:\n```text\nonedrive --get-sharepoint-drive-id '<your site name to search>'\n```\nThis will return something similar to the following:\n```text\nConfiguration file successfully loaded\nConfiguring Global Azure AD Endpoints\nInitializing the Synchronization Engine ...\nOffice 365 Library Name Query: <your site name to search>\n-----------------------------------------------\nSite Name:    <your site name>\nLibrary Name: <your library name>\ndrive_id:     b!6H_y8B...xU5\nLibrary URL:  <your library URL>\n-----------------------------------------------\n```\nIf there are no matches to the site you are attempting to search, the following will be displayed:\n```text\nConfiguration file successfully loaded\nConfiguring Global Azure AD Endpoints\nInitializing the Synchronization Engine ...\nOffice 365 Library Name Query: blah\n\nERROR: The requested SharePoint site could not be found. Please check it's name and your permissions to access the site.\n\nThe following SharePoint site names were returned:\n * <site name 1>\n * <site name 2>\n ...\n * <site name X>\n```\nThis list of site names can be used as a basis to search for the correct site for which you are searching\n\n## 3. Create a new configuration directory and sync location for this SharePoint Library\nCreate a new configuration directory for this SharePoint Library in the following manner:\n```text\nmkdir ~/.config/SharePoint_My_Library_Name\n```\n\nCreate a new local folder to store the SharePoint Library data in:\n```text\nmkdir ~/SharePoint_My_Library_Name\n```\n\n> [!TIP]\n> Do not use spaces in the directory name, use '_' as a replacement\n\n## 4. Configure SharePoint Library config file with the required 'drive_id' & 'sync_dir' options\nDownload a copy of the default configuration file by downloading this file from GitHub and saving this file in the directory created above:\n```text\nwget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/SharePoint_My_Library_Name/config\n```\n\nUpdate your 'onedrive' configuration file (`~/.config/SharePoint_My_Library_Name/config`) with the local folder where you will store your data:\n```text\nsync_dir = \"~/SharePoint_My_Library_Name\"\n```\n\nUpdate your 'onedrive' configuration file(`~/.config/SharePoint_My_Library_Name/config`) with the 'drive_id' value obtained in the steps above:\n```text\ndrive_id = \"insert the drive_id value from above here\"\n```\nThe OneDrive client will now be configured to sync this SharePoint shared library to your local system and the location you have configured.\n\n> [!IMPORTANT]\n> After changing `drive_id`, you must perform a full re-synchronization by adding `--resync` to your existing command line.\n\n## 5. Validate and Test the configuration\nValidate your new configuration using the `--display-config` option to validate you have configured the application correctly:\n```text\nonedrive --confdir=\"~/.config/SharePoint_My_Library_Name\" --display-config\n```\n\nTest your new configuration using the `--dry-run` option to validate the application configuration:\n```text\nonedrive --confdir=\"~/.config/SharePoint_My_Library_Name\" --synchronize --verbose --dry-run\n```\n\n> [!IMPORTANT]\n> 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.\n\n## 6. Sync the SharePoint Library as required\nSync the SharePoint Library to your system with either `--synchronize` or `--monitor` operations:\n```text\nonedrive --confdir=\"~/.config/SharePoint_My_Library_Name\" --synchronize --verbose\n```\n\n```text\nonedrive --confdir=\"~/.config/SharePoint_My_Library_Name\" --monitor --verbose\n```\n\n> [!IMPORTANT]\n> 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.\n\n## 7. Enable custom systemd service for SharePoint Library\nSystemd 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\n\nIn 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:\n*   RHEL / CentOS: `/usr/lib/systemd/system`\n*   Others: `/usr/lib/systemd/user` and `/lib/systemd/system`\n\n### Step1: Create a new systemd service file\n#### Red Hat Enterprise Linux, CentOS Linux\nCopy the required service file to a new name:\n```text\nsudo cp /usr/lib/systemd/system/onedrive.service /usr/lib/systemd/system/onedrive-SharePoint_My_Library_Name.service\n```\nor \n```text\nsudo cp /usr/lib/systemd/system/onedrive@.service /usr/lib/systemd/system/onedrive-SharePoint_My_Library_Name@.service\n```\n\n#### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora\nCopy the required service file to a new name:\n```text\nsudo cp /usr/lib/systemd/user/onedrive.service /usr/lib/systemd/user/onedrive-SharePoint_My_Library_Name.service\n```\nor \n```text\nsudo cp /lib/systemd/system/onedrive@.service /lib/systemd/system/onedrive-SharePoint_My_Library_Name@.service\n```\n\n### Step 2: Edit new systemd service file\nEdit the new systemd file, updating the line beginning with `ExecStart` so that the confdir mirrors the one you used above:\n```text\nExecStart=/usr/local/bin/onedrive --monitor --confdir=\"/full/path/to/config/dir\"\n```\n\nExample:\n```text\nExecStart=/usr/local/bin/onedrive --monitor --confdir=\"/home/myusername/.config/SharePoint_My_Library_Name\"\n```\n\n> [!IMPORTANT]\n> 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.\n\n### Step 3: Enable the new systemd service\nOnce the file is correctly edited, you can enable the new systemd service using the following commands.\n\n#### Red Hat Enterprise Linux, CentOS Linux\n```text\nsystemctl enable onedrive-SharePoint_My_Library_Name\nsystemctl start onedrive-SharePoint_My_Library_Name\n```\n\n#### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora\n```text\nsystemctl --user enable onedrive-SharePoint_My_Library_Name\nsystemctl --user start onedrive-SharePoint_My_Library_Name\n```\nor\n```text\nsystemctl --user enable onedrive-SharePoint_My_Library_Name@myusername.service\nsystemctl --user start onedrive-SharePoint_My_Library_Name@myusername.service\n```\n\n### Step 4: Viewing systemd status and logs for the custom service\n#### Viewing systemd service status - Red Hat Enterprise Linux, CentOS Linux\n```text\nsystemctl status onedrive-SharePoint_My_Library_Name\n```\n\n#### Viewing systemd service status - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora\n```text\nsystemctl --user status onedrive-SharePoint_My_Library_Name\n```\n\n#### Viewing journalctl systemd logs - Red Hat Enterprise Linux, CentOS Linux\n```text\njournalctl --unit=onedrive-SharePoint_My_Library_Name -f\n```\n\n#### Viewing journalctl systemd logs - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora\n```text\njournalctl --user --unit=onedrive-SharePoint_My_Library_Name -f\n```\n\n### Step 5: (Optional) Run custom systemd service at boot without user login\nIn some cases it may be desirable for the systemd service to start without having to login as your 'user'\n\nAll 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.\n\nTo 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:\n```text\nloginctl enable-linger <your_user_name>\n```\n\nExample:\n```text\nalex@ubuntu-headless:~$ loginctl enable-linger alex\n```\n\n## 8. Configuration for a SharePoint Library is complete\nThe 'onedrive' client configuration for this particular SharePoint Library is now complete.\n\n# How to configure multiple OneDrive SharePoint Shared Library sync\nCreate a new configuration as per the process above. Repeat these steps for each SharePoint Library that you wish to use.\n"
  },
  {
    "path": "docs/terms-of-service.md",
    "content": "# OneDrive Client for Linux - Software Service Terms of Service\n\n## 1. Introduction\n\nThese 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.\n\n## 2. License Compliance\n\nThe 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\n\n## 3. Use of the Service\n\n### 3.1. Access and Accounts\n\nYou 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.\n\n### 3.2. Prohibited Activities\n\nYou agree not to:\n\n- Use the Service in any way that violates applicable laws or regulations.\n- Use the Service to engage in any unlawful, harmful, or fraudulent activity.\n- Use the Service in any manner that disrupts, damages, or impairs the Service.\n\n## 4. Intellectual Property\n\nThe 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.\n\n## 5. Disclaimer of Warranties\n\nThe 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.\n\nMicrosoft 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\n\nOneDrive Client for Linux is not responsible for the Microsoft OneDrive Service or the Microsoft Graph API Service that this Application utilises. Any issue with either Microsoft OneDrive or Microsoft Graph API should be raised with Microsoft via their support channel in your country.\n\n## 6. Limitation of Liability\n\nTo 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.\n\nThis limitation of liability explicitly relates to the use of the OneDrive Client for Linux software and does not affect your rights under the GPLv3.\n\n## 7. Changes to Terms\n\nWe 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.\n\n## 8. Governing Law\n\nThese Terms shall be governed by and construed in accordance with the laws of Australia, without regard to its conflict of law principles.\n\n## 9. Contact Us\n\nIf you have any questions or concerns about these Terms, please contact us at https://github.com/abraunegg/onedrive or email support@mynas.com.au\n\n"
  },
  {
    "path": "docs/ubuntu-package-install.md",
    "content": "# Installation of 'onedrive' package on Debian and Ubuntu\n\nThis document outlines the steps for installing the 'onedrive' client on Debian, Ubuntu, and their derivatives using the OpenSuSE Build Service Packages.\n\n> [!CAUTION]\n> This information is specifically for the following platforms and distributions:\n> * Debian\n> * Deepin\n> * Elementary OS\n> * Kali Linux\n> * Lubuntu\n> * Linux Mint\n> * MX Linux\n> * Pop!_OS\n> * Peppermint OS\n> * Raspbian | Raspberry Pi OS\n> * Ubuntu | Kubuntu | Xubuntu | Ubuntu Mate\n> * Zorin OS\n>\n> 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.\n\n> [!IMPORTANT]\n> The distribution versions listed below are **End-of-Life (EOL)** and are **no longer supported** or tested with current client releases. You must upgrade to a supported distribution before proceeding.\n> * Debian 9\n> * Debian 10\n> * Ubuntu 16.x\n> * Ubuntu 18.x\n> * Ubuntu 20.x\n\n\n## Determine which instructions to use\nUbuntu 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.\n\n### Step 1: Remove any configured PPA and associated 'onedrive' package and systemd service files\n\n#### Step 1a: Remove PPA if configured\nMany 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.\n\nTo remove the yann1ck PPA repository, perform the following actions:\n```text\nsudo add-apt-repository --remove ppa:yann1ck/onedrive\n```\n\n#### Step 1b: Remove 'onedrive' package installed from Debian / Ubuntu repositories\nMany Internet 'help' pages provide inconsistent details on how to install the OneDrive Client for Linux. A number of these websites continue to advise users to install the client via `sudo apt install onedrive` without first configuring the OpenSuSE Build Service (OBS) Repository. When installing without OBS, you install an obsolete client version with known bugs that have been fixed, but this package also contains an errant systemd service (see below) that impacts background running of this client.\n\nTo remove the Ubuntu Universe client, perform the following actions:\n```text\nsudo apt remove onedrive\n```\n\n#### Step 1c: Remove errant systemd service file installed by Debian / Ubuntu distribution packages\n\nThe Debian and Ubuntu distribution packages automatically create and enable a default user-level systemd service when installing the onedrive package so that the client runs automatically after authentication. During installation you may see:\n\n```\nCreated symlink /etc/systemd/user/default.target.wants/onedrive.service → /usr/lib/systemd/user/onedrive.service.\n```\n\nThis systemd entry is not part of this project’s installation model and is introduced by Debian/Ubuntu packaging defaults. It should be removed. If left in place, it can cause the following error:\n\n```\nOpening the item database ...\n\nERROR: onedrive application is already running - check system process list for active application instances\n - Use 'sudo ps aufxw | grep onedrive' to potentially determine active running process\n\nWaiting for all internal threads to complete before exiting application\n```\nAs the client is built with GUI notifications enabled, each automatic restart of this service may also spam your desktop with notifications.\n\nTo remove this symbolic link created by the distribution package, run:\n\n```\nsudo rm /etc/systemd/user/default.target.wants/onedrive.service\n```\n\nIf this service is not removed, uninstalling the `onedrive` package may result in repeated systemd restart attempts and log entries similar to:\n```\nFeb 10 10:32:00 host systemd[USER_A]: Started onedrive.service - OneDrive Client for Linux.\nFeb 10 10:32:00 host (onedrive)[PID_A]: onedrive.service: Unable to locate executable '/usr/bin/onedrive': No such file or directory\nFeb 10 10:32:00 host (onedrive)[PID_A]: onedrive.service: Failed at step EXEC spawning /usr/bin/onedrive: No such file or directory\nFeb 10 10:32:00 host systemd[USER_A]: onedrive.service: Main process exited, code=exited, status=203/EXEC\nFeb 10 10:32:00 host systemd[USER_A]: onedrive.service: Failed with result 'exit-code'.\nFeb 10 10:32:02 host systemd[USER_B]: Started onedrive.service - OneDrive Client for Linux.\nFeb 10 10:32:02 host (onedrive)[PID_B]: onedrive.service: Unable to locate executable '/usr/bin/onedrive': No such file or directory\nFeb 10 10:32:02 host (onedrive)[PID_B]: onedrive.service: Failed at step EXEC spawning /usr/bin/onedrive: No such file or directory\nFeb 10 10:32:02 host systemd[USER_B]: onedrive.service: Main process exited, code=exited, status=203/EXEC\nFeb 10 10:32:02 host systemd[USER_B]: onedrive.service: Failed with result 'exit-code'.\nFeb 10 10:32:03 host systemd[USER_A]: onedrive.service: Scheduled restart job, restart counter is at 201.\nFeb 10 10:32:03 host systemd[USER_A]: Starting onedrive.service - OneDrive Client for Linux...\nFeb 10 10:32:05 host systemd[USER_B]: onedrive.service: Scheduled restart job, restart counter is at 105.\nFeb 10 10:32:05 host systemd[USER_B]: Starting onedrive.service - OneDrive Client for Linux...\n\n```\n\nThis behaviour originates from Debian/Ubuntu packaging defaults and does not occur with the OpenSuSE Build Service packages.\n\n\n### Step 2: Ensure your system is up-to-date\nUse a script, similar to the following to ensure your system is updated correctly:\n```text\n#!/bin/bash\nrm -rf /var/lib/dpkg/lock-frontend\nrm -rf /var/lib/dpkg/lock\napt-get update\napt-get upgrade -y\napt-get dist-upgrade -y\napt-get autoremove -y\napt-get autoclean -y\n```\n\nRun this script as 'root' by using `su -` to elevate to 'root'. Example below:\n```text\nWelcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.0-36-generic x86_64)\n\n * Documentation:  https://help.ubuntu.com\n * Management:     https://landscape.canonical.com\n * Support:        https://ubuntu.com/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n0 updates can be applied immediately.\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\n\nThe list of available updates is more than a week old.\nTo check for new updates run: sudo apt update\nLast login: Mon Nov 10 06:42:58 2025 from xxx.xxx.xxx.xxx\nalex@ubuntu-24-04:~$ su -\nPassword: \nroot@ubuntu-24-04:~# ls -la\ntotal 36\ndrwx------  5 root root 4096 Nov 10 06:43 .\ndrwxr-xr-x 23 root root 4096 Jun 30  2024 ..\n-rw-------  1 root root  168 Nov 10 06:43 .bash_history\n-rw-r--r--  1 root root 3106 Apr 22  2024 .bashrc\ndrwx------  2 root root 4096 Apr 24  2024 .cache\n-rw-r--r--  1 root root  161 Apr 22  2024 .profile\ndrwx------  6 root root 4096 Jun 30  2024 snap\ndrwx------  2 root root 4096 Jun 30  2024 .ssh\n-rwxr-xr-x  1 root root  174 Nov 10 06:43 update_os.sh\nroot@ubuntu-24-04:~# cat update_os.sh \n#!/bin/bash\nrm -rf /var/lib/dpkg/lock-frontend\nrm -rf /var/lib/dpkg/lock\napt-get update\napt-get upgrade -y\napt-get dist-upgrade -y\napt-get autoremove -y\napt-get autoclean -y\nroot@ubuntu-24-04:~# ./update_os.sh \nGet:1 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]\nHit:2 http://au.archive.ubuntu.com/ubuntu noble InRelease                \nGet:3 http://au.archive.ubuntu.com/ubuntu noble-updates InRelease [126 kB]\nGet:4 http://au.archive.ubuntu.com/ubuntu noble-backports InRelease [126 kB]\nGet:5 http://au.archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages [1,585 kB]\n....\nUnpacking libglx-mesa0:amd64 (25.0.7-0ubuntu0.24.04.2) over (24.0.5-1ubuntu1) ...\nPreparing to unpack .../6-libgl1-amber-dri_21.3.9-0ubuntu3~24.04.1_amd64.deb ...\nUnpacking libgl1-amber-dri:amd64 (21.3.9-0ubuntu3~24.04.1) over (21.3.9-0ubuntu2) ...\n(Reading database ... 152058 files and directories currently installed.)\nRemoving libglapi-mesa:amd64 (24.0.5-1ubuntu1) ...\nSelecting previously unselected package libglapi-amber:amd64.\n(Reading database ... 152049 files and directories currently installed.)\nPreparing to unpack .../00-libglapi-amber_21.3.9-0ubuntu3~24.04.1_amd64.deb ...\nUnpacking libglapi-amber:amd64 (21.3.9-0ubuntu3~24.04.1) ...\nSelecting previously unselected package libmalcontent-0-0:amd64.\nPreparing to unpack .../01-libmalcontent-0-0_0.11.1-1ubuntu1.2_amd64.deb ...\nUnpacking libmalcontent-0-0:amd64 (0.11.1-1ubuntu1.2) ...\nPreparing to unpack .../02-gnome-control-center_1%3a46.7-0ubuntu0.24.04.2_amd64.deb ...\nUnpacking gnome-control-center (1:46.7-0ubuntu0.24.04.2) over (1:46.0.1-1ubuntu7) ...\nPreparing to unpack .../03-libxatracker2_25.0.7-0ubuntu0.24.04.2_amd64.deb ...\nUnpacking libxatracker2:amd64 (25.0.7-0ubuntu0.24.04.2) over (24.0.5-1ubuntu1) ...\nSelecting previously unselected package linux-modules-6.14.0-35-generic.\nPreparing to unpack .../04-linux-modules-6.14.0-35-generic_6.14.0-35.35~24.04.1_amd64.deb ...\nUnpacking linux-modules-6.14.0-35-generic (6.14.0-35.35~24.04.1) ...\nSelecting previously unselected package linux-image-6.14.0-35-generic.\nPreparing to unpack .../05-linux-image-6.14.0-35-generic_6.14.0-35.35~24.04.1_amd64.deb ...\nUnpacking linux-image-6.14.0-35-generic (6.14.0-35.35~24.04.1) ...\nSelecting previously unselected package linux-modules-extra-6.14.0-35-generic.\nPreparing to unpack .../06-linux-modules-extra-6.14.0-35-generic_6.14.0-35.35~24.04.1_amd64.deb ...\n....\nDel libpam-modules-bin 1.5.3-5ubuntu5.1 [51.9 kB]\nDel systemd-sysv 255.4-1ubuntu8.1 [11.9 kB]\nroot@ubuntu-24-04:~# \n```\n\nReboot your system after running this process before continuing with Step 3. This ensures that your system is correctly up-to-date and any prior running 'onedrive' process and systemd service is now correctly removed and not running.\n```text\nreboot\n```\n\n### Step 3: Determine what your OS is based on\nDetermine what your OS is based on. To do this, run the following command:\n```text\nlsb_release -a\n```\n**Example:**\n```text\nalex@ubuntu-24-04:~$ lsb_release -a\nNo LSB modules are available.\nDistributor ID: Ubuntu\nDescription:    Ubuntu 24.04 LTS\nRelease:        24.04\nCodename:       noble\nalex@ubuntu-24-04:~$ \n```\n\n### Step 4: Pick the correct instructions to use\nIf required, review the table below based on your 'lsb_release' information to pick the appropriate instructions to use:\n\n| Release & Codename | Instructions to use |\n|--------------------|---------------------|\n| Linux Mint 19.x           | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Linux Mint 22.x |\n| Linux Mint 20.x           | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Linux Mint 22.x |\n| Linux Mint 21.x           | Use [Ubuntu 22.04](#distribution-ubuntu-2204) instructions below |\n| Linux Mint 22.x           | Use [Ubuntu 24.04](#distribution-ubuntu-2404) instructions below |\n| Linux Mint Debian Edition (LMDE) 5 / Elsie | Use [Debian 11](#distribution-debian-11) instructions below |\n| Linux Mint Debian Edition (LMDE) 6 / Faye  | Use [Debian 12](#distribution-debian-12) instructions below |\n| Linux Mint Debian Edition (LMDE) 7 / Gigi  | Use [Debian 13](#distribution-debian-13) instructions below |\n| Debian 9 / stretch        | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Debian 13 |\n| Debian 10 / buster        | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Debian 13 |\n| Debian 11 / bullseye      | Use [Debian 11](#distribution-debian-11) instructions below |\n| Debian 12 / bookworm      | Use [Debian 12](#distribution-debian-12) instructions below |\n| Debian 13 / trixie        | Use [Debian 13](#distribution-debian-13) instructions below |\n| Debian Sid                | Refer to https://packages.debian.org/sid/onedrive for assistance |\n| Raspbian GNU/Linux 10     | You must build from source or upgrade your Operating System to Raspbian GNU/Linux 12 |\n| Raspbian GNU/Linux 11     | Use [Debian 11](#distribution-debian-11) instructions below |\n| Raspbian GNU/Linux 12     | Use [Debian 12](#distribution-debian-12) instructions below |\n| Ubuntu 16.04 / Xenial     | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 24.04 |\n| Ubuntu 18.04 / Bionic     | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 24.04 |\n| Ubuntu 20.04 / Focal      | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 24.04 |\n| Ubuntu 21.04 / Hirsute    | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 24.04 |\n| Ubuntu 21.10 / Impish     | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 24.04 |\n| Ubuntu 22.04 / Jammy      | Use [Ubuntu 22.04](#distribution-ubuntu-2204) instructions below |\n| Ubuntu 22.10 / Kinetic    | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 24.04 |\n| Ubuntu 23.04 / Lunar      | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 24.04 |\n| Ubuntu 23.10 / Mantic     | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 24.04 |\n| Ubuntu 24.04 / Noble      | Use [Ubuntu 24.04](#distribution-ubuntu-2404) instructions below |\n| Ubuntu 24.10 / Oracular   | This platform is **End-of-Life (EOL)** and no longer supported. You must upgrade to at least Ubuntu 25.04 |\n| Ubuntu 25.04 / Plucky     | Use [Ubuntu 25.04](#distribution-ubuntu-2504) instructions below |\n| Ubuntu 25.10 / Questing   | Use [Ubuntu 25.10](#distribution-ubuntu-2510) instructions below |\n\n> [!IMPORTANT]\n> If your Linux distribution or release is **not listed in the table above**, you have two options:\n>\n> 1. Compile the client from source. Refer to [Installing or Upgrading the OneDrive Client for Linux](install.md).\n> 2. Request packaging support from your distribution’s maintainers so that an official, supported package can be provided.\n\n## Distribution Package Install Instructions\n\n### Distribution: Debian 11\nThe packages support the following platform architectures:\n| &nbsp;i686&nbsp; | x86_64 | ARMHF | AARCH64 |\n|:----:|:------:|:-----:|:-------:|\n|✔|✔|✔|✔|\n\n#### Step 1: Add the OpenSuSE Build Service repository release key\nAdd the OpenSuSE Build Service repository release key using the following command:\n```text\nwget -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\n```\n\n#### Step 2: Add the OpenSuSE Build Service repository\nAdd the OpenSuSE Build Service repository using the following command:\n```text\necho \"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\n```\n\n#### Step 3: Update your apt package cache\nRun: `sudo apt-get update`\n\n#### Step 4: Install 'onedrive'\nRun: `sudo apt install --no-install-recommends --no-install-suggests onedrive`\n\n#### Step 5: Read 'Known Issues' with these packages\nRead and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed.\n\n### Distribution: Debian 12\nThe packages support the following platform architectures:\n| &nbsp;i686&nbsp; | x86_64 | ARMHF | AARCH64 |\n|:----:|:------:|:-----:|:-------:|\n|✔|✔|✔|✔|\n\n#### Step 1: Add the OpenSuSE Build Service repository release key\nAdd the OpenSuSE Build Service repository release key using the following command:\n```text\nwget -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\n```\n\n#### Step 2: Add the OpenSuSE Build Service repository\nAdd the OpenSuSE Build Service repository using the following command:\n```text\necho \"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\n```\n\n#### Step 3: Update your apt package cache\nRun: `sudo apt-get update`\n\n#### Step 4: Install 'onedrive'\nRun: `sudo apt install --no-install-recommends --no-install-suggests onedrive`\n\n#### Step 5: Read 'Known Issues' with these packages\nRead and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed.\n\n### Distribution: Debian 13\nThe packages support the following platform architectures:\n| &nbsp;i686&nbsp; | x86_64 | ARMHF | AARCH64 |\n|:----:|:------:|:-----:|:-------:|\n|✔|✔|✔|✔|\n\n#### Step 1: Add the OpenSuSE Build Service repository release key\nAdd the OpenSuSE Build Service repository release key using the following command:\n```text\nwget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_13/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null\n```\n\n#### Step 2: Add the OpenSuSE Build Service repository\nAdd the OpenSuSE Build Service repository using the following command:\n```text\necho \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_13/ ./\" | sudo tee /etc/apt/sources.list.d/onedrive.list\n```\n\n#### Step 3: Update your apt package cache\nRun: `sudo apt-get update`\n\n#### Step 4: Install 'onedrive'\nRun: `sudo apt install --no-install-recommends --no-install-suggests onedrive`\n\n#### Step 5: Read 'Known Issues' with these packages\nRead and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed.\n\n### Distribution: Ubuntu 22.04\nThe packages support the following platform architectures:\n| &nbsp;i686&nbsp; | x86_64 | ARMHF | AARCH64 |\n|:----:|:------:|:-----:|:-------:|\n|❌|✔|✔|✔|\n\n#### Step 1: Add the OpenSuSE Build Service repository release key\nAdd the OpenSuSE Build Service repository release key using the following command:\n```text\nwget -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\n```\n\n#### Step 2: Add the OpenSuSE Build Service repository\nAdd the OpenSuSE Build Service repository using the following command:\n```text\necho \"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\n```\n\n#### Step 3: Update your apt package cache\nRun: `sudo apt-get update`\n\n#### Step 4: Install 'onedrive'\nRun: `sudo apt install --no-install-recommends --no-install-suggests onedrive`\n\n#### Step 5: Read 'Known Issues' with these packages\nRead and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed.\n\n### Distribution: Ubuntu 24.04\nThe packages support the following platform architectures:\n| &nbsp;i686&nbsp; | x86_64 | ARMHF | AARCH64 |\n|:----:|:------:|:-----:|:-------:|\n|❌|✔|✔|✔|\n\n#### Step 1: Add the OpenSuSE Build Service repository release key\nAdd the OpenSuSE Build Service repository release key using the following command:\n```text\nwget -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\n```\n\n#### Step 2: Add the OpenSuSE Build Service repository\nAdd the OpenSuSE Build Service repository using the following command:\n```text\necho \"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\n```\n\n#### Step 3: Update your apt package cache\nRun: `sudo apt-get update`\n\n#### Step 4: Install 'onedrive'\nRun: `sudo apt install --no-install-recommends --no-install-suggests onedrive`\n\n#### Step 5: Read 'Known Issues' with these packages\nRead and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed.\n\n### Distribution: Ubuntu 25.04\nThe packages support the following platform architectures:\n| &nbsp;i686&nbsp; | x86_64 | ARMHF | AARCH64 |\n|:----:|:------:|:-----:|:-------:|\n|❌|✔|✔|✔|\n\n#### Step 1: Add the OpenSuSE Build Service repository release key\nAdd the OpenSuSE Build Service repository release key using the following command:\n```text\nwget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_25.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null\n```\n\n#### Step 2: Add the OpenSuSE Build Service repository\nAdd the OpenSuSE Build Service repository using the following command:\n```text\necho \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_25.04/ ./\" | sudo tee /etc/apt/sources.list.d/onedrive.list\n```\n\n#### Step 3: Update your apt package cache\nRun: `sudo apt-get update`\n\n#### Step 4: Install 'onedrive'\nRun: `sudo apt install --no-install-recommends --no-install-suggests onedrive`\n\n#### Step 5: Read 'Known Issues' with these packages\nRead and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed.\n\n### Distribution: Ubuntu 25.10\nThe packages support the following platform architectures:\n| &nbsp;i686&nbsp; | x86_64 | ARMHF | AARCH64 |\n|:----:|:------:|:-----:|:-------:|\n|❌|✔|✔|✔|\n\n#### Step 1: Add the OpenSuSE Build Service repository release key\nAdd the OpenSuSE Build Service repository release key using the following command:\n```text\nwget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_25.10/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null\n```\n\n#### Step 2: Add the OpenSuSE Build Service repository\nAdd the OpenSuSE Build Service repository using the following command:\n```text\necho \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_25.10/ ./\" | sudo tee /etc/apt/sources.list.d/onedrive.list\n```\n\n#### Step 3: Update your apt package cache\nRun: `sudo apt-get update`\n\n#### Step 4: Install 'onedrive'\nRun: `sudo apt install --no-install-recommends --no-install-suggests onedrive`\n\n#### Step 5: Read 'Known Issues' with these packages\nRead and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed.\n\n\n## Known Issues with Installing from the above packages\nThere are currently no known issues when installing 'onedrive' from the OpenSuSE Build Service repository.\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# Using the OneDrive Client for Linux\n## Application Version\nBefore 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.\n\n## Table of Contents\n\n- [Important Notes](#important-notes)\n  - [Memory Usage](#memory-usage)\n  - [Guidelines for Local File and Folder Naming in the Synchronisation Directory](#guidelines-for-local-file-and-folder-naming-in-the-synchronisation-directory)\n  - [Support for Microsoft Azure Information Protected Files](#support-for-microsoft-azure-information-protected-files)\n  - [Compatibility with Editors and Applications Using Atomic Save Operations](#compatibility-with-editors-and-applications-using-atomic-save-operations)\n  - [Compatibility with Obsidian](#compatibility-with-obsidian)\n  - [Compatibility with curl](#compatibility-with-curl)\n- [First Steps](#first-steps)\n  - [Authorise the Application with Your Microsoft OneDrive Account](#authorise-the-application-with-your-microsoft-onedrive-account)\n  - [Display Your Applicable Runtime Configuration](#display-your-applicable-runtime-configuration)\n  - [Understanding OneDrive Client for Linux Operational Modes](#understanding-onedrive-client-for-linux-operational-modes)\n    - [Standalone Synchronisation Operational Mode (Standalone Mode)](#standalone-synchronisation-operational-mode-standalone-mode)\n    - [Ongoing Synchronisation Operational Mode (Monitor Mode)](#ongoing-synchronisation-operational-mode-monitor-mode)\n- [Using the OneDrive Client for Linux to synchronise your data](#using-the-onedrive-client-for-linux-to-synchronise-your-data)\n  - [Client Documentation](#client-documentation)\n  - [Increasing application logging level](#increasing-application-logging-level)\n  - [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)\n  - [Why 'Server Side Filtering' is not possible with Microsoft OneDrive](#why-server-side-filtering-is-not-possible-with-microsoft-onedrive)\n  - [Testing your configuration](#testing-your-configuration)\n  - [Performing a sync with Microsoft OneDrive](#performing-a-sync-with-microsoft-onedrive)\n  - [Performing a single directory synchronisation with Microsoft OneDrive](#performing-a-single-directory-synchronisation-with-microsoft-onedrive)\n  - [Performing a 'one-way' download synchronisation with Microsoft OneDrive](#performing-a-one-way-download-synchronisation-with-microsoft-onedrive)\n  - [Performing a 'one-way' upload synchronisation with Microsoft OneDrive](#performing-a-one-way-upload-synchronisation-with-microsoft-onedrive)\n  - [Performing a selective synchronisation via 'sync_list' file](#performing-a-selective-synchronisation-via-sync_list-file)\n  - [Performing a --resync](#performing-a---resync)\n  - [Performing a --force-sync without a --resync or changing your configuration](#performing-a---force-sync-without-a---resync-or-changing-your-configuration)\n  - [Enabling the Client Activity Log](#enabling-the-client-activity-log)\n    - [Client Activity Log Example:](#client-activity-log-example)\n    - [Client Activity Log Differences](#client-activity-log-differences)\n  - [Display Manager Integration](#display-manager-integration)\n  - [GUI Notifications](#gui-notifications)\n  - [Using a local Recycle Bin](#using-a-local-recycle-bin)\n  - [Handling a Microsoft OneDrive Account Password Change](#handling-a-microsoft-onedrive-account-password-change)\n  - [Determining the synchronisation result](#determining-the-synchronisation-result)\n  - [Resumable Transfers](#resumable-transfers)\n  \n- [Frequently Asked Configuration Questions](#frequently-asked-configuration-questions)\n  - [How to change the default configuration of the client?](#how-to-change-the-default-configuration-of-the-client)\n  - [How to change where my data from Microsoft OneDrive is stored?](#how-to-change-where-my-data-from-microsoft-onedrive-is-stored)\n  - [Why does the client create 'safeBackup' files?](#why-does-the-client-create-safebackup-files)\n  - [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)\n  - [How are uploads and downloads managed?](#how-are-uploads-and-downloads-managed)\n  - [How to only sync a specific directory?](#how-to-only-sync-a-specific-directory)\n  - [How to 'skip' files from syncing?](#how-to-skip-files-from-syncing)\n  - [How to 'skip' directories from syncing?](#how-to-skip-directories-from-syncing)\n  - [How to 'skip' .files and .folders from syncing?](#how-to-skip-files-and-folders-from-syncing)\n  - [How to 'skip' files larger than a certain size from syncing?](#how-to-skip-files-larger-than-a-certain-size-from-syncing)\n  - [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)\n  - [How can I prevent my local disk from filling up?](#how-can-i-prevent-my-local-disk-from-filling-up)\n  - [How does the client handle symbolic links?](#how-does-the-client-handle-symbolic-links)\n  - [How to synchronise OneDrive Personal Shared Folders?](#how-to-synchronise-onedrive-personal-shared-folders)\n  - [How to synchronise OneDrive Business Shared Items (Files and Folders)?](#how-to-synchronise-onedrive-business-shared-items-files-and-folders)\n  - [How to synchronise SharePoint / Office 365 Shared Libraries?](#how-to-synchronise-sharepoint--office-365-shared-libraries)\n  - [How to Create a Shareable Link?](#how-to-create-a-shareable-link)\n  - [How to Synchronise Both Personal and Business Accounts at once?](#how-to-synchronise-both-personal-and-business-accounts-at-once)\n  - [How to Synchronise Multiple SharePoint Libraries simultaneously?](#how-to-synchronise-multiple-sharepoint-libraries-simultaneously)\n  - [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)\n  - [How to initiate the client as a background service?](#how-to-initiate-the-client-as-a-background-service)\n    - [OneDrive service running as root user via init.d](#onedrive-service-running-as-root-user-via-initd)\n    - [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)\n    - [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)\n    - [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)\n    - [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)\n    - [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)\n  - [How to start a user systemd service at boot without user login?](#how-to-start-a-user-systemd-service-at-boot-without-user-login)\n  - [How to access Microsoft OneDrive service through a proxy](#how-to-access-microsoft-onedrive-service-through-a-proxy)\n  - [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)\n- [Advanced Configuration of the OneDrive Client for Linux](#advanced-configuration-of-the-onedrive-client-for-linux)\n- [Overview of all OneDrive Client for Linux CLI Options](#overview-of-all-onedrive-client-for-linux-cli-options)\n\n## Important Notes\n\n### Memory Usage\nStarting 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.\n\nDuring 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.\n\nTo 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.\n\n### Guidelines for Local File and Folder Naming in the Synchronisation Directory\nTo 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.\n\n> [!WARNING]\n> 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.\n\n#### Key Restrictions and Limitations\n* Invalid Characters: \n  * Avoid using the following characters in names of files and folders: `\" * : < > ? / \\ |`\n  * Names should not start or end with spaces\n  * Names should not end with a fullstop / period character `.`\n* Prohibited Names: \n  * 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 `~$`\n  * The text sequence `_vti_` cannot appear anywhere in a file or directory name\n  * A file and folder called `forms` is unsupported at the root level of a synchronisation directory\n* Path Length\n  * All files and folders stored in your 'sync_dir' (typically `~/OneDrive`) must not have a path length greater than:\n    * 400 characters for OneDrive Business & SharePoint\n    * 430 characters for OneDrive Personal\n\nShould 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.\n\n> [!TIP]\n> 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:\n> | Standard Invalid Character | Potential UTF-16 Replacement Character |\n> |--------------------|------------------------------|\n> | .                  | ․ (One Dot Leader, `\\u2024`)  |\n> | :                  | ː (Modifier Letter Triangular Colon, `\\u02D0`) |\n> | \\|                 | │ (Box Drawings Light Vertical, `\\u2502`)       |\n\n> [!CAUTION]\n> 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.\n\n#### Further reading:\nThe 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:\n* [Microsoft Windows Naming Conventions](https://docs.microsoft.com/windows/win32/fileio/naming-a-file)\n* [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)\n\n**Adherence to these guidelines is not optional but mandatory to avoid sync disruptions.**\n\n### Support for Microsoft Azure Information Protected Files\n> [!CAUTION]\n> 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.\n>\n> 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.\n> \n> 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.\n> \n> Please use the `--disable-download-validation` option with extreme caution and understand the risk if you enable it.\n\n### Compatibility with Editors and Applications Using Atomic Save Operations\n\nMany modern editors and applications—including `vi`, `vim`, `nvim`, `emacs`, `LibreOffice`, `Obsidian` and others—use *atomic save* strategies to preserve data integrity when writing files. This section outlines how such operations interact with the `onedrive` client, what users can expect, and why certain side effects (such as editor warnings or perceived timestamp discrepancies) may occur.\n\n#### How Atomic Save Operations Work\n\nWhen these applications save a file, they typically follow this sequence:\n\n1. **Create a Temporary File**  \n   A new file is written with the updated content, often in the same directory as the original.\n\n2. **Flush to Disk**  \n   The temporary file is flushed to disk using `fsync()` or an equivalent method to ensure data safety.\n\n3. **Atomic Rename**  \n   The temporary file is renamed to the original filename using the `rename()` syscall.  \n   This is an atomic operation on Linux, meaning the original file is *replaced*, not modified.\n\n4. **Remove Lock or Swap Files**  \n   Auxiliary files used during editing (e.g., `.swp`, `.#filename`) are deleted.\n\nAs a result, the saved file is **technically a new file** with a new inode and a new timestamp, even if the filename remains unchanged.\n\n#### How This Affects the OneDrive Client\n\nWhen the `onedrive` client observes such an atomic save operation via `inotify`, it detects:\n\n- The original file as *deleted*.\n- A new file (with the same name) as *created*.\n\nThe client responds accordingly:\n\n- The \"new\" file is uploaded to Microsoft OneDrive.\n- After upload, Microsoft assigns its own *modification timestamp* to the file.\n- To ensure consistency between local and remote states, the client updates the local file’s timestamp to match the **exact time** stored in OneDrive.\n\n> [!IMPORTANT]\n> Microsoft OneDrive does **not support fractional-second precision** in file timestamps—only whole seconds. As a result, small discrepancies may occur if the local file system supports higher-resolution timestamps.\n\nThis behaviour ensures accurate syncing and content integrity, but may lead to subtle side effects in timestamp-sensitive applications.\n\n#### Expected Side Effects\n\n- **Timestamp Alignment for Atomic Saves**  \n  Editors that rely on local file timestamps (rather than content checksums) can issue warnings that a file had changed unexpectedly—typically because the `onedrive` client potentially updated the modification time after upload.\n  This client attempts to preserve the original modification timestamp only if fractional seconds differ, preventing unnecessary local timestamp changes. As a result, editors such as `vi`, `vim`, `nvim`, `emacs`, `LibreOffice` and `Obsidian` should not trigger warnings when saving files using atomic operations.\n\n- **False Conflict Prompts (Collaborative Editing)**  \n  In collaborative editing scenarios—such as with LibreOffice or shared OneDrive folders—conflict prompts may still occur if another user or device modifies a file, resulting in a meaningful timestamp or content change.  \n  However, for local edits using atomic save methods, the client now avoids unnecessary timestamp updates, effectively eliminating false conflicts in those cases.\n\n#### Recommendation\n\nIf you are using editors that rely on strict timestamp semantics and wish to minimise interference from the `onedrive` client:\n\n- Save your work, then pause or temporarily stop sync (`onedrive --monitor`).\n- Resume syncing when finished.\n- Configure the client to ignore temporary files your editor uses via the `skip_file` setting if they do not need to be synced.\n- Configure the client to use 'session uploads' for all files via the `force_session_upload` setting. This option, when enabled, forces the client to use a 'session' upload, which, when the 'file' is uploaded by the session, this includes the actual local timestamp (without fractional seconds) of the file that Microsoft OneDrive should store.\n\n#### Summary\n\nThe `onedrive` client is fully compatible with applications that use atomic save operations. Users should be aware that:\n\n- Atomic saves result in the file being treated as a new item.\n- Timestamps may be adjusted post-upload to match OneDrive's stored value.\n- In rare cases, timestamp-sensitive applications may display warnings or prompts.\n\nThis behaviour is by design and ensures consistency and data integrity between your local filesystem and the OneDrive cloud.\n\n### Compatibility with Obsidian\nObsidian on Linux does not provide a built-in way to disable atomic saves or switch to a backup-copy method via configuration. The application is built on Electron and relies on the default save behaviour of its underlying libraries and editor components (such as CodeMirror), which typically perform *atomic writes* using the following process:\n\n1. A temporary file is created containing the updated content.\n2. That temporary file is flushed to disk.\n3. The temporary file is atomically renamed to replace the original file.\n\nThis behaviour is intended to improve data integrity and crash resilience, but it results in high disk I/O — particularly in Obsidian, where auto-save is triggered nearly every keystroke.\n\n> [!IMPORTANT]\n> Obsidian provides no mechanism to change how this save behaviour operates. This is a serious design limitation and should be treated as a bug in the application. The excessive and unnecessary write operations can significantly reduce the lifespan of SSDs over time due to increased wear, leading to broader consequences for system reliability.\n\n#### How This Affects the OneDrive Client\n\nBecause Obsidian is constantly writing files, running the OneDrive Client for Linux in `--monitor` mode causes the client to continually receive inotify events from the local file system. This leads to constant re-uploading of files, regardless of whether meaningful content has changed.\n\nThe consequences of this are:\n\n1. Continuous upload attempts to Microsoft OneDrive.\n2. Potential for repeated overwrites of online data.\n3. Excessive API usage, which may result in Microsoft throttling your access — subsequently affecting the client’s ability to synchronise files reliably.\n\n#### Recommendation\n\nIf you use Obsidian, it is *strongly* recommended that you enable the following two configuration options in your OneDrive Client for Linux `config` file:\n```\nforce_session_upload = \"true\"\ndelay_inotify_processing = \"true\"\n```\nThese settings introduce a delay in processing local file change events, allowing the OneDrive Client for Linux to batch or debounce Obsidian's frequent writes. By default, this delay is 5 seconds.\n\nTo adjust this delay, you can add the following configuration option:\n```\ninotify_delay = \"10\"\n```\nThis example sets the delay to 10 seconds.\n\n> [!CAUTION]\n> Increasing `inotify_delay` too aggressively may have unintended side effects. All file system events are queued and processed in order, so setting a very high delay could result in large backlogs or undesirable data synchronisation outcomes — particularly in cases of rapid file changes or deletions.\n>\n> Adjust this setting with extreme caution and test thoroughly to ensure it does not impact your workflow or data integrity.\n\n> [!TIP]\n> An Obsidian Plugin also exists to 'control' the auto save behaviour of Obsidian.\n> \n> Instead of saving every two seconds from start of typing (Obsidian default), this plugin makes Obsidian wait for the user to finish with editing, and after the input stops, it waits for a defined time (by default 10 seconds) and then it only saves once.\n> \n> For more information please read: https://github.com/mihasm/obsidian-autosave-control \n\n\n### Compatibility with curl\nIf 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.\n\nFor 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.\n\nHowever, 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.\n\nIf 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.\n\n#### Known curl bugs that impact the use of this client\n| id | curl bug | fixed in curl version |\n|----|----------|-----------------------|\n| 1  | HTTP/2 support: Introduced HTTP/2 support, enabling multiplexed transfers over a single connection | 7.47.0 |\n| 2  | HTTP/2 issue: Resolved an issue where HTTP/2 connections were not properly reused, leading to unnecessary new connections. | 7.68.0 |\n| 3  | HTTP/2 issue: Addressed a race condition in HTTP/2 multiplexing that could lead to unexpected behaviour. | 7.74.0 |\n| 4  | HTTP/2 issue: Improved handling of HTTP/2 priority frames to ensure proper stream prioritisation. | 7.81.0 |\n| 5  | HTTP/2 issue: Fixed a bug where HTTP/2 connections were prematurely closed, resulting in incomplete data transfers. | 7.88.1 |\n| 6  | HTTP/2 issue: Resolved a problem with HTTP/2 frame handling that could cause data corruption during transfers. | 8.2.1 |\n| 7  | HTTP/2 issue: Corrected an issue where HTTP/2 streams were not properly closed, leading to potential memory leaks. | 8.5.0 |\n| 8  | HTTP/2 issue: Addressed a bug where HTTP/2 connections could hang under specific conditions, improving reliability. | 8.8.0 |\n| 9  | HTTP/2 issue: Improved handling of HTTP/2 connections to prevent unexpected stream resets and enhance stability. | 8.9.0 |\n| 10 | SIGPIPE issue: Resolved a problem where SIGPIPE signals were not properly handled, leading to unexpected behaviour. | 8.9.1 |\n| 11 | SIGPIPE issue: Addressed a SIGPIPE leak that occurred in certain cases starting with version 8.9.1 | 8.10.0 |\n| 12 | HTTP/2 issue: Stopped offering ALPN `http/1.1` for `http2-prior-knowledge` to ensure proper protocol negotiation. | 8.10.0 |\n| 13 | HTTP/2 issue: Improved handling of end-of-stream (EOS) and blocked states to prevent unexpected behaviour.| 8.11.0 |\n| 14 | OneDrive operation encountered an issue with libcurl reading the local SSL CA Certificates issue | 8.14.1 |\n\n#### Known curl versions with compatibility issues for this client\n| curl Version | distribution | curl bugs |\n|--------------|--------------|-----------|\n| 7.68.0       | Ubuntu 20.04 LTS (Focal Fossa) | 2,3,4,5,6,7,8,9,10,11,12,13 |\n| 7.74.0       | Debian 11 (Bullseye) | 4,5,6,7,8,9,10,11,12,13 |\n| 7.81.0       | Ubuntu 22.04 LTS (Jammy Jellyfish) | 5,6,7,8,9,10,11,12,13 |\n| 7.88.1       | Debian 12 (Bookworm) | 6,7,8,9,10,11,12,13 |\n| 8.2.1        | Alpine Linux 3.14 | 7,8,9,10,11,12,13 |\n| 8.5.0        | Alpine Linux 3.15, Ubuntu 24.04 LTS (Noble Numbat) | 8,9,10,11,12,13 |\n| 8.9.1        | Ubuntu 24.10 (Oracular Oriole) | 11,12,13 |\n| 8.10.0       | Alpine Linux 3.17 | 13 |\n| 8.13.0       | Various + Self Compiled | 14 |\n| 8.13.1       | Various + Self Compiled | 14 |\n| 8.14.0       | Various + Self Compiled | 14 |\n\n> [!IMPORTANT]\n> 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.\n>\n> If you are using one of the above curl versions, the following application message will be generated:\n> ```text\n> WARNING: Your curl/libcurl version (curl.version.number) has known HTTP/2 bugs that impact the use of this application.\n>          Please report this to your distribution and request that they provide a newer curl version for your platform or upgrade this yourself.\n>          Downgrading all application operations to use HTTP/1.1 to ensure maximum operational stability.\n>          Please read https://github.com/abraunegg/onedrive/blob/master/docs/usage.md#compatibility-with-curl for more information.\n> ```\n>\n> 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:\n> 1. Upgrade your curl version on your platform\n> 2. Configure the client to always downgrade client operations to HTTP/1.1 and use IPv4 only\n>\n> 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:\n> ```text\n> force_http_11 = \"true\"\n> ip_protocol_version = \"1\"\n> ```\n> When these two options are applied to your application configuration, the following application message will be generated:\n> ```text\n> WARNING: Your curl/libcurl version (curl.version.number) has known operational bugs that impact the use of this application.\n>          Please report this to your distribution and request that they provide a newer curl version for your platform or upgrade this yourself.\n> ```\n>\n> The WARNING line will be now only be written to application logging output, no longer sending a GUI notification message.\n\n> [!IMPORTANT]\n> 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`\n>\n> The only options to resolve this issue are the following:\n> 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.\n> 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\"`\n\n> [!IMPORTANT]\n> 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.\n> \n> 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: \n> \n> * `A libcurl timeout has been triggered - data transfer too slow, no DNS resolution response, no server response`\n> * `Could not connect to server on handle ABC12DEF3456`\n>\n> The only options to resolve this issue are the following:\n> 1. Implement and/or ensure that IPv6 DNS resolution is possible on your system; allow IPv6 network connectivity between your system and Microsoft OneDrive\n> 2. Configure the client to only use IPv4 DNS resolution via setting the configuration option `ip_protocol_version = \"1\"`\n\n> [!IMPORTANT]\n> If you are using Debian 12 or Linux Mint Debian Edition (LMDE) 6, you can install the latest curl version from the respective backports repositories to address the bugs present in the default Debian 12 curl version.\n\n> [!CAUTION]\n> If you continue to use a curl/libcurl version with known HTTP/2 bugs the application will automatically downgrade HTTP operations to HTTP/1.1, however you will continue to experience application runtime issues such as randomly exiting for zero reason or incomplete download/upload of your data.\n\n## First Steps\n\n### Authorise the Application with Your Microsoft OneDrive Account\nOnce 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.\n\nPlease be aware that some organisations 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.\n\nThis client supports the following methods to authenticate the application with Microsoft OneDrive:\n* Supports interactive browser-based authentication using OAuth2 and a redirect URI\n* Supports seamless Single Sign-On (SSO) via Intune using the Microsoft Identity Device Broker D-Bus interface\n* Supports OAuth2 Device Authorisation Flow for Microsoft Entra ID accounts\n\n#### Interactive Authentication using OAuth2 and a redirect URI\nWhen you run the application for the first time, you'll be prompted to open a specific URL in your web browser. This URL takes you to the Microsoft login page, where you’ll sign in with your Microsoft Account and grant the application permission to access your files.\n\nAfter granting permission, your browser will redirect you to a blank page, or a page that displays this message: \n\n![microsoft-auth-display-message](./images/microsoft-auth-display-message.png)\n\nThis is expected behaviour.\n\nAt this point, copy the full redirect URI shown in your browser's address bar and paste it into the terminal where prompted.\n\n**Example Terminal Session:**\n```text\nuser@hostname:~$ onedrive\nD-Bus message bus daemon is available; GUI notifications are now enabled\nUsing IPv4 and IPv6 (if configured) for all network operations\nAttempting to contact Microsoft OneDrive Login Service\nSuccessfully reached Microsoft OneDrive Login Service\nConfiguring Global Azure AD Endpoints\n\nPlease authorise this application by visiting the following URL:\n\nhttps://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\n\nAfter completing the authorisation in your browser, copy the full redirect URI (from the address bar) and paste it below.\n\nPaste redirect URI here: https://login.microsoftonline.com/common/oauth2/nativeclient?code=<redacted>\n\nThe application has been successfully authorised, but no extra command options have been specified.\n\nPlease use 'onedrive --help' for further assistance in regards to running this application.\n\nuser@hostname:~$ \n\n```\n\n**Interactive OAuth2 Authentication Process Illustrated:**\n![initial_auth_url_access_redacted](./images/initial_auth_url_access_redacted.png)\n\n![copy_redirect_uri_to_application](./images/authorise_client_before_copy_with_arrow.png)\n\n![copy_redirect_uri_to_application_done](./images/authorise_client_after_paste_hashed_out.png)\n\n![client_authorised](./images/authorise_client_now_authorised_hashed_out.png)\n\n> [!IMPORTANT]\n> Without additional input or configuration, the OneDrive Client for Linux will automatically adhere to default application settings during synchronisation processes with Microsoft OneDrive.\n\n> [!IMPORTANT]\n> **Handling a AADSTS70000 response**\n>\n> If you paste the redirect URI back into the CLI and receive:\n> `AADSTS70000: The provided value for the 'code' parameter is not valid.`\n> this is **not a client bug**.\n>\n> Microsoft authorisation codes are single-use and short-lived, so the code you pasted is no longer redeemable.\n>\n> **Common causes:**\n> * Browser extensions / privacy tools modifying the redirect URL (for example, ad-blockers or 'remove tracking parameters' features within browsers)\n> * Copying the wrong URL (ensure you copy from the browser address bar immediately after consent)\n> * Refreshing the page or reusing the same redirect URI (codes can only be redeemed once)\n> * Waiting too long before pasting the URL back\n>\n> **Remediation steps for AADSTS70000:**\n> 1. Re-run: `onedrive --reauth`\n> 2. Use a private/incognito browser session or a clean browser profile\n> 3. Temporarily disable URL-filtering/privacy extensions for the Microsoft login pages (uBlock Origin / ClearURLs / Brave Shields / similar), then retry\n\n\n#### Single Sign-On (SSO) via Intune using the Microsoft Identity Device Broker \nTo use this method of authentication, you must add the following configuration to your 'config' file:\n```\nuse_intune_sso = \"true\"\n```\nThe application will check to ensure that Intune is operational and that the required dbus elements are available. Should these be available, the following will be displayed:\n```\n...\nClient has been configured to use Intune SSO via Microsoft Identity Broker dbus session - checking usage criteria\nIntune SSO via Microsoft Identity Broker dbus session usage criteria met - will attempt to authenticate via Intune\n...\n```\n> [!NOTE]\n> The installation and configuration of Intune for your platform is beyond the scope of this documentation.\n\n#### OAuth2 Device Authorisation Flow for Microsoft Entra ID accounts\nTo use this method of authentication, you must add the following configuration to your 'config' file:\n```\nuse_device_auth = \"true\"\n```\nYou will be required to open a URL using a web browser, and enter the code that this application presents:\n```\nConfiguring Global Azure AD Endpoints\n\nAuthorise this application by visiting:\n\nhttps://microsoft.com/devicelogin\n\nEnter the following code when prompted: ABCDEFGHI\n\nThis code expires at: 2025-Jun-02 15:27:30\n```\nYou will have ~15 minutes before the code expires.\n\n> [!IMPORTANT]\n> #### Limitation: OAuth2 Device Authorization Flow and Personal Microsoft Accounts\n>\n> While the OneDrive Client for Linux fully supports OAuth2 Device Authorisation Flow (`device_code` grant) for **Microsoft Entra ID (Work/School)** accounts, **Microsoft currently does not allow this flow to be used with personal Microsoft accounts (MSA)** unless the application is explicitly authorised by Microsoft.\n>\n> **Application Configuration Summary:**\n>\n> - `signInAudience`: `AzureADandPersonalMicrosoftAccount`\n> - `allowPublicClient`: `true`\n> - Uses Microsoft Identity Platform v2.0 endpoints (`/devicecode`, `/token`, etc.)\n> - Microsoft Graph scopes properly defined\n>\n> Despite this correct configuration, users signing in with a Personal Microsoft OneDrive account will see the following error:\n>\n> > **\"The code you entered has expired. Get a new code from the device you're trying to sign in to and try again.\"**\n>\n> This occurs even if the code is entered immediately. Microsoft redirects the user to:\n>\n> ```\n> https://login.live.com/ppsecure/post.srf?username=......\n> ```\n>\n> This behaviour confirms that Microsoft **blocks the `device_code` grant flow for MSA accounts** on unapproved (by Microsoft) applications.\n>\n> **Recommendation:**  \n> If using a Personal Microsoft OneDrive account (e.g., @outlook.com or @hotmail.com), please complete authentication using the interactive authentication method detailed above.\n>\n> **Further Reading:**  \n> 📚 [Microsoft Documentation — OAuth 2.0 device authorisation grant](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code)\n\n### Display Your Applicable Runtime Configuration\nTo verify the configuration that the application will use, use the following command:\n```text\nonedrive --display-config\n```\nThis command will display all the relevant runtime interpretations of the options and configurations you are using. An example output is as follows:\n```text\nReading configuration file: /home/user/.config/onedrive/config\nConfiguration file successfully loaded\nonedrive version                             = vX.Y.Z-A-bcdefghi\nConfig path                                  = /home/user/.config/onedrive\nConfig file found in config path             = true\nConfig option 'drive_id'                     = \nConfig option 'sync_dir'                     = ~/OneDrive\n...\nConfig option 'webhook_enabled'              = false\n```\n\n> [!IMPORTANT]\n> 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.\n\n### Understanding OneDrive Client for Linux Operational Modes\nThere are two modes of operation when using the client:\n1. Standalone sync mode that performs a single sync action against Microsoft OneDrive.\n2. Ongoing sync mode that continuously syncs your data with Microsoft OneDrive.\n\n> [!TIP]\n> To understand further the client operational modes and how the client operates, please review the [client architecture](client-architecture.md) documentation.\n\n> [!IMPORTANT]\n> 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.\n\n#### Standalone Synchronisation Operational Mode (Standalone Mode)\nThis method of use can be employed by issuing the following option to the client:\n```text\nonedrive --sync\n```\nFor simplicity, this can be shortened to the following:\n```text\nonedrive -s\n```\n\n#### Ongoing Synchronisation Operational Mode (Monitor Mode)\nThis method of use can be utilised by issuing the following option to the client:\n```text\nonedrive --monitor\n```\nFor simplicity, this can be shortened to the following:\n```text\nonedrive -m\n```\n> [!NOTE]\n> This method of use is used when enabling a systemd service to run the application in the background.\n\nTwo common errors can occur when using monitor mode:\n*   Initialisation failure\n*   Unable to add a new inotify watch\n\nBoth 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:\n*   Open Files Soft limit (current session)\n*   Open Files Hard limit (current session)\n*   `fs.inotify.max_user_watches`\n\nTo determine what the existing values are on your system, use the following commands:\n\n**open files**\n```text\nulimit -Sn\nulimit -Hn\n```\n\n**inotify watches**\n```text\nsysctl fs.inotify.max_user_watches\n```\n\nAlternatively, when running the client with increased verbosity (see below), the client will display what the current configured system maximum values are:\n```text\n...\nAll application operations will be performed in the configured local 'sync_dir' directory: /home/alex/OneDrive\nOneDrive synchronisation interval (seconds): 300\nMaximum allowed open files (soft):           1024\nMaximum allowed open files (hard):           262144\nMaximum allowed inotify user watches:        29463\nInitialising filesystem inotify monitoring ...\n...\n```\nTo determine what value to change to, you need to count all the files and folders in your configured 'sync_dir' location:\n```text\ncd /path/to/your/sync/dir\nls -laR | wc -l\n```\n\nTo make a change to these variables using your file and folder count, use the following process:\n\n**open files**\n\nYou can increase the limits for your current shell session temporarily using:\n```\nulimit -n <new_value>\n```\nRefer to your distribution documentation to make the change persistent across reboots and sessions. \n\n> [!NOTE]\n> systemd overrides these values for user sessions and services. If you are making a system wide change that is persistent across reboots and sessions you will also have to modify your systemd service files in the following manner:\n> ```\n> [Service]\n> LimitNOFILE=<new_value>\n> ```\n> Post the modification of systemd service files you will need to reload and restart the services.\n\n**inotify watches**\n```text\nsudo sysctl fs.inotify.max_user_watches=<new_value>\n```\nOnce these values are changed, you will need to restart your client so that the new values are detected and used.\n\nTo make these changes permanent on your system, refer to your OS reference documentation.\n\n## Using the OneDrive Client for Linux to synchronise your data\n\n### Client Documentation\n\nThe following documents provide detailed guidance on installing, configuring, and using the OneDrive Client for Linux:\n\n* **[advanced-usage.md](https://github.com/abraunegg/onedrive/blob/master/docs/advanced-usage.md)**\n  Instructions for advanced configurations, including multiple account setups, Docker usage, dual-boot scenarios, and syncing to mounted directories.\n\n* **[application-config-options.md](https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md)**\n  Comprehensive list and explanation of all configuration file and command-line options available in the client.\n\n* **[application-security.md](https://github.com/abraunegg/onedrive/blob/master/docs/application-security.md)**\n  Details on security considerations and practices related to the OneDrive client.\n\n* **[business-shared-items.md](https://github.com/abraunegg/onedrive/blob/master/docs/business-shared-items.md)**\n  Instructions on syncing shared items in OneDrive for Business accounts.\n\n* **[client-architecture.md](https://github.com/abraunegg/onedrive/blob/master/docs/client-architecture.md)**\n  Overview of the client's architecture and design principles.\n\n* **[docker.md](https://github.com/abraunegg/onedrive/blob/master/docs/docker.md)**\n  Instructions for running the OneDrive client within Docker containers.\n\n* **[known-issues.md](https://github.com/abraunegg/onedrive/blob/master/docs/known-issues.md)**\n  List of known issues and limitations of the OneDrive client.\n\n* **[national-cloud-deployments.md](https://github.com/abraunegg/onedrive/blob/master/docs/national-cloud-deployments.md)**\n  Information on deploying the client in national cloud environments.\n\n* **[podman.md](https://github.com/abraunegg/onedrive/blob/master/docs/podman.md)**\n  Guide for running the OneDrive client using Podman containers.\n\n* **[sharepoint-libraries.md](https://github.com/abraunegg/onedrive/blob/master/docs/sharepoint-libraries.md)**\n  Instructions for syncing SharePoint document libraries.\n\n* **[ubuntu-package-install.md](https://github.com/abraunegg/onedrive/blob/master/docs/ubuntu-package-install.md)**\n  Specific instructions for installing the client on Ubuntu systems.\n\n* **[webhooks.md](https://github.com/abraunegg/onedrive/blob/master/docs/webhooks.md)**\n  Information on configuring and using webhooks with the OneDrive client.\n\nFurther documentation not listed above can be found here: https://github.com/abraunegg/onedrive/blob/master/docs/\n\nPlease read these additional references to assist you with installing, configuring, and using the OneDrive Client for Linux.\n\n### Increasing application logging level\nWhen 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. \n\nThe client supports four levels of logging output:\n\n#### 1. Normal (default)\nOnly essential information is shown — suitable for standard usage without additional output.\n\n#### 2. Verbose \nEnables general status and progress information. Use:\n```text\nonedrive --sync --verbose\n```\nor its short form:\n```text\nonedrive -s -v\n```\n\n#### 3. Debug Logging\nEnables detailed internal logging useful for diagnosing issues. This is activated by specifying the `--verbose` flag twice:\n```text\nonedrive --sync --verbose --verbose\n```\n\n#### 4. HTTPS Debug Logging\nEnables full debug logging including HTTPS request/response information. This is typically only needed for advanced debugging of API or network issues. Activate with:\n```text\nonedrive --sync --verbose --verbose --debug-https\n```\n\n> [!IMPORTANT]\n> When raising a bug report or attempting to understand unexpected behaviour, it is recommended to enable debug logging using `--verbose --verbose`.\n>\n> Only use `--debug-https` if explicitly requested or required, as it may expose sensitive information in logs.\n\n### Using 'Client Side Filtering' rules to determine what should be synced with Microsoft OneDrive\nClient 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:\n\n* **check_nosync:** This option allows you to create a `.nosync` file in local directories, to skip that directory from being included in sync operations.\n* **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.\n* **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.\n* **skip_file:** Specific files can be excluded from synchronisation using this option. It provides flexibility in selecting which files are essential for cloud storage.\n* **skip_size:** Skip files greater than this specific size (in MB)\n* **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.\n\nAdditionally, 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.\n\nThese 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.\n\n> [!IMPORTANT]\n> Client Side Filtering rules are generally processed in the following order:\n> 1. 'check_nosync'\n> 2. 'skip_dotfiles'\n> 3. 'skip_symlinks'\n> 4. 'skip_dir'\n> 5. 'skip_file'\n> 6. 'sync_list'\n> 7. 'skip_size'\n>\n> This can be best illustrated below:\n> \n> ![Client Side Filtering Processing Order](./puml/client_side_filtering_processing_order.png)\n>\n> For further details please review the [client architecture](client-architecture.md) documentation.\n\n> [!IMPORTANT]\n> After changing any Client Side Filtering rule, you must perform a full re-synchronisation by using `--resync`.\n\n### Why 'Server Side Filtering' is not possible with Microsoft OneDrive\n\nIt is important to understand that all filtering performed by this client (including `sync_list`) is client-side filtering. Microsoft OneDrive and the Microsoft Graph API do not support server-side selective sync or the ability to apply include/exclude rules when retrieving data. The client must first enumerate the remote filesystem to understand its structure and state, and only then apply filtering rules locally to determine what should be synchronised. This behaviour is expected and is a direct result of platform limitations, not a defect in the client.\n\nFor further details please read the [server-side filtering limitations](server-side-filtering-limitations.md) documentation.\n\n### Testing your configuration\nYou 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:\n```text\nonedrive --sync --verbose --dry-run\nReading configuration file: /home/user/.config/onedrive/config\nConfiguration file successfully loaded\nUsing 'user' Config Dir: /home/user/.config/onedrive\nDRY-RUN Configured. Output below shows what 'would' have occurred.\nDRY-RUN: Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations\nDRY RUN: Not creating backup config file as --dry-run has been used\nDRY RUN: Not updating hash files as --dry-run has been used\nChecking Application Version ...\nAttempting to initialise the OneDrive API ...\nConfiguring Global Azure AD Endpoints\nThe OneDrive API was initialised successfully\nOpening the item database ...\nSync Engine Initialised with new Onedrive API instance\nApplication version:  vX.Y.Z-A-bcdefghi\nAccount Type:         <account-type>\nDefault Drive ID:     <drive-id>\nDefault Root ID:      <root-id>\nRemaining Free Space: 1058488129 KB\nAll application operations will be performed in: /home/user/OneDrive\nFetching items from the OneDrive API for Drive ID: <drive-id> ..\n...\nPerforming a database consistency and integrity check on locally stored data ... \nProcessing DB entries for this Drive ID: <drive-id>\nProcessing ~/OneDrive\nThe directory has not changed\n...\nScanning local filesystem '~/OneDrive' for new data to upload ...\n...\nPerforming a final true-up scan of online data from Microsoft OneDrive\nFetching items from the OneDrive API for Drive ID: <drive-id> ..\n\nSync with Microsoft OneDrive is complete\n```\n\n### Performing a sync with Microsoft OneDrive\nBy default, all files are downloaded in `~/OneDrive`. This download location is controlled by the 'sync_dir' config option.\n\nAfter authorising the application, a sync of your data can be performed by running:\n```text\nonedrive --sync\n```\nThis will synchronise files from your Microsoft OneDrive account to your `~/OneDrive` local directory or to your specified 'sync_dir' location.\n\n> [!TIP]\n> #### Specifying the 'source of truth' for your synchronisation with Microsoft OneDrive\n> By default, the OneDrive Client for Linux treats your online OneDrive data as the source of truth. This means that when determining which version of a file should be trusted as authoritative, the client prioritises the state of files stored online over local copies.\n> \n> In some workflows, you may prefer to treat your local files as the primary reference instead — for example, when you regularly make changes locally and want those to take precedence during conflict resolution.\n> \n> To change this behaviour, enable the local-first mode by setting the configuration option in your `config` file:\n> ```text\n> local_first = \"true\"\n> ```\n> or by using the command-line argument at runtime:\n> ```text\n> onedrive --sync --local-first\n> ```\n> \n> When this option is enabled, the client will prioritise local data as the source of truth when comparing file differences and resolving synchronisation conflicts.\n> \n\n### Performing a single directory synchronisation with Microsoft OneDrive\nIn 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:\n```text\nonedrive --sync --single-directory '<dir_name>'\n```\n\n> [!TIP]\n> If the full path is `~/OneDrive/mydir`, the command would be `onedrive --sync --single-directory 'mydir'`\n\n### Performing a 'one-way' download synchronisation with Microsoft OneDrive\nIn some cases, it may be desirable to 'download only' from Microsoft OneDrive. To do this, use the following command:\n```text\nonedrive --sync --download-only\n```\nThis 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.\n\n> [!IMPORTANT]\n> There is an application functionality change between v2.4.x and v.2.5x when using this option.\n>\n> 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.\n>\n> 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.\n\n> [!TIP]\n> If you have the requirement to clean up local files that have been removed online, use the following command:\n> ```text\n> onedrive --sync --download-only --cleanup-local-files\n> ```\n\n### Performing a 'one-way' upload synchronisation with Microsoft OneDrive\nIn 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:\n\n```text\nonedrive --sync --upload-only\n```\n\n> [!IMPORTANT]\n> - 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.\n> - 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.\n\n> [!TIP]\n> 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:\n> ```text\n> onedrive --sync --upload-only --no-remote-delete\n> ```\n\n> [!IMPORTANT]\n> - `--upload-only`: This command will only upload local changes to OneDrive. These changes can include additions, modifications, moves, and deletions of files and folders.\n> - `--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.\n\n### Performing a selective synchronisation via 'sync_list' file\nSelective synchronisation allows you to sync only specific files and directories.\n\nTo enable selective synchronisation, create a file named `sync_list` in your application configuration directory (default is `~/.config/onedrive`).\n\n> [!IMPORTANT]\n> Important points to understand before using 'sync_list'.\n> *    'sync_list' excludes _everything_ by default on OneDrive.\n> *    'sync_list' follows an _\"exclude overrides include\"_ rule, and requires **explicit inclusion**.\n> *    Order specific exclusions before inclusions, so that anything _specifically included_ is included.\n> *    How and where you place your `/` matters for excludes and includes in subdirectories.\n\nEach 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. \n\n> [!CAUTION]\n> Rules without slashes (`Codes`, `Work`, `Backup`, `notes.txt`, etc.) are the most expensive form of `sync_list` rule as this instructs the client to scan every folder online & local to find a match. As a result, these types of rules can cause:\n> * High CPU usage\n> * High disk or network activity\n> * Increased fan usage (especially on laptops)\n>\n> If you want best performance, always prefer fully-qualified or path-scoped 'sync_list' rules. Avoid generic includes unless absolutely necessary.\n\n#### Example 'sync_list' rules\n```text\n# ======================================================================\n# Example sync_list\n# ======================================================================\n# IMPORTANT:\n# - 'sync_list' EXCLUDES EVERYTHING by default.\n# - Exclusions come first.\n# - Inclusions follow.\n#\n# Matching behaviour:\n# - Rules WITHOUT a slash (e.g., \"Backup\", \"notes.txt\") match ANYWHERE.\n#   ⚠️ These rules force exhaustive scanning of ALL online and local folders.\n#   ⚠️ They are computationally expensive.\n#\n# - Rules with a leading \"/\" apply ONLY to the OneDrive ROOT.\n#\n# - Rules with a trailing \"/\" match DIRECTORIES only.\n#\n# Wildcards and globbing:\n# - \"*\"   matches any characters within a single path segment.\n# - \"**\"  matches directories RECURSIVELY across ANY depth.\n# ======================================================================\n\n# ----------------------------------------------------------------------\n# EXCLUSIONS (ALWAYS PUT THESE FIRST)\n# ----------------------------------------------------------------------\n\n# Exclude temporary folders inside ANY Documents folder (any level)\n!Documents/temp*\n\n# Exclude Secret_data ONLY in OneDrive root\n!/Secret_data/*\n\n# ----------------------------------------------------------------------\n# Modern development / programming exclusions\n# (Common cache/build folders used by many languages & tools)\n# ----------------------------------------------------------------------\n\n# Python virtual environments\n!venv/*\n!.venv/*\n!__pycache__/*\n\n# Node.js / JavaScript build directories\n!node_modules/*\n!.next/*\n\n# Java & Kotlin build caches\n!build/kotlin/*\n!.kotlin/*\n\n# Gradle build system cache\n!.gradle/*\n\n# JetBrains IDE caches\n!.idea/libraries/*\n!.idea/caches/*\n\n# Generic runtime caches\n!.cache/*\n\n\n# ----------------------------------------------------------------------\n# INCLUSIONS (WHAT YOU *DO* WANT TO SYNC)\n# ----------------------------------------------------------------------\n\n# Include the Backup folder OR any file/folder named \"Backup\" ANYWHERE.\n# ⚠️ High-cost rule — causes full tree scanning.\nBackup\n\n# Include Documents directory ANYWHERE\n# ⚠️ High-cost rule — causes full tree scanning.\nDocuments/\n\n# Include all PDF files inside any Documents folder\n# ⚠️ High-cost rule — causes full tree scanning.\nDocuments/*.pdf\n\n# Include one specific file, if present inside ANY Documents folder\n# ⚠️ High-cost rule — causes full tree scanning.\nDocuments/latest_report.docx\n\n# Include the /Backup/ folder ONLY in the OneDrive root\n/Backup/\n\n# Include Blender ONLY if in root\n/Blender\n\n\n# ----------------------------------------------------------------------\n# PROJECT / DEVELOPMENT STRUCTURES WITH WILDCARDS & GLOBBING\n# ----------------------------------------------------------------------\n\n# Include any folder or file beginning with \"Project\" inside ANY Work/ folder\n# ⚠️ High-cost rule — causes full tree scanning.\nWork/Project*\n\n# Include the 'Blog' directory — and ONLY that specific folder\n# .\n# ├── Parent\n# │   ├── Blog\n# │   │   ├── random_files\n# │   │   │   ├── CZ9aZRM7U1j7pM21fH0MfP2gywlX7bqW\n# │   │   │   └── k4GptfTBE2z2meRFqjf54tnvSXcXe30Y\n# │   │   └── random_images\n# │   │       ├── cAuQMfX7qsMIOmzyQYdELikZwsXeCYsL\n# │   │       └── GqjZuo7UBB0qjYM2WUcZXOvToAhCQ29M\n# │   └── other_stuffs\n#\n/Parent/Blog/*\n\n# Include Android build directories located ANYWHERE inside ANY project\n!/Programming/Projects/Android/**/build/*\n\n# Include Android NDK /.cxx build trees ANYWHERE inside ANY project\n!/Programming/Projects/Android/**/.cxx/*\n\n# Include Web build output directories across ANY nested depth\n!/Programming/Projects/Web/**/build/*\n\n# Include the entire /Programming directory from OneDrive root\n/Programming\n\n\n# ----------------------------------------------------------------------\n# FILE-BY-NAME MATCHING ANYWHERE\n# ----------------------------------------------------------------------\n\n# Match all files named exactly \"notes.txt\" ANYWHERE\n# ⚠️ High-cost rule — causes full tree scanning.\nnotes.txt\n\n\n# ----------------------------------------------------------------------\n# DIRECTORIES WITH SPACES\n# ----------------------------------------------------------------------\n# - There is zero requirement to escape space sequences within the 'sync_list' file\n\n# Include directories under ANY Pictures folder\n# ⚠️ High-cost rule — causes full tree scanning.\nPictures/Camera Roll\nPictures/Saved Pictures\n\n# Include 'Camera Roll' and all files / folders \n/Pictures/Camera Roll/*\n\n# Include 'Saved Pictures' and all files / folders \n/Pictures/Saved Pictures/*\n\n\n# ----------------------------------------------------------------------\n# GENERIC NAME MATCHES (⚠️ VERY EXPENSIVE)\n# These match ANY file or folder with that name ANYWHERE in OneDrive.\n# They cause full, exhaustive scanning of ALL online and local folders.\n# ----------------------------------------------------------------------\n\nCinema Soc\nCodes\nTextbooks\nYear 2\nDocuments\nPictures\nMusic\n\n```\nThe following are supported for pattern matching and exclusion rules:\n*   Use the `*` to wildcard select any characters to match for the item to be included\n*   Use either `!` or `-` characters at the start of the line to exclude an otherwise included item\n\n> [!IMPORTANT]\n> 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`\n\n> [!TIP]\n> 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.\n\n> [!TIP] \n> 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:\n> ```text\n> sync_root_files = \"true\"\n> ```\n> 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.\n\n### Performing a --resync\nA `--resync` operation instructs the client to delete its local state database and fully rebuild it from the current online OneDrive contents. This is a powerful recovery and re-alignment action that should be used **sparingly** and **with care**.\n\n> [!IMPORTANT]\n> **Do not use --resync as part of normal or routine operation.**\n>\n> A `--resync` is **not** a “refresh” or “force sync” button. It is a destructive recovery action that discards the client’s local sync history and forces a rebuild based solely on the current online OneDrive state.\n>\n> Habitually using `--resync` has several negative impacts:\n> * It removes the historical sync context the client uses to safely resolve conflicts.\n> * It can cause unnecessary uploads, downloads, and renames.\n> * It increases the chance of triggering rate-limiting (HTTP 429 responses) from the Microsoft Graph API.\n> * It can mask underlying configuration or permission issues that should be properly diagnosed instead.\n>\n> If you are unsure whether the client is in sync, do not run `--resync`. Instead, use:\n>```\n> onedrive --display-sync-status\n>```\n> Only use `--resync` when the client explicitly requests it or when a documented configuration change requires it.\n\n#### When a --resync is required\nYou **must** perform a `--resync` after modifying any of the following configuration items:\n\n* `check_nosync`\n* `drive_id`\n* `sync_dir`\n* `skip_file`\n* `skip_dir`\n* `skip_dotfiles`\n* `skip_size`\n* `skip_symlinks`\n* `sync_business_shared_items`\n* Creating, modifying, or deleting the `sync_list` file\n\nYou may also use `--resync` if you believe the local state has become inconsistent with online OneDrive state. However, if you only want to check the current sync status, run:\n```text\nonedrive --display-sync-status\n```\n\nThis shows whether you are up-to-date without requiring a resynchronisation.\n\n#### What happens when you use `--resync`\n\nWhen invoking `--resync`, the client displays one of the following prompts depending on the client version.\n\n#### v2.5.9 and below\n```text\nThe usage of --resync will delete your local 'onedrive' client state, thus no record of your current 'sync status' will exist.\nThis has the potential to overwrite local versions of files with perhaps older versions of documents downloaded from OneDrive, resulting in local data loss.\nIf in doubt, backup your local data before using --resync\n\nAre you sure you wish to proceed with --resync? [Y/N]\n```\n\n#### v2.5.10 and above\n```text\nWARNING: You have asked the client to perform a --resync operation.\n\n         This operation will delete the client’s local state database and rebuild it entirely from the current online OneDrive state.\n\n         Because the previous sync state will no longer be available, the following may occur:\n         * Local files that also exist in OneDrive may have local changes overwritten by the cloud version if a conflict cannot be safely resolved.\n         * Local files may be renamed or duplicated locally as part of conflict resolution and data-preservation handling.\n         * The initial synchronisation pass may involve a large number of file uploads and downloads.\n         * The increased activity against the Microsoft Graph API may trigger HTTP 429 (throttling) responses during the synchronisation process.\n\n         For safest operation:\n         * Ensure you have a current backup of your sync_dir.\n         * Run this command first with --dry-run to confirm all planned actions.\n         * Enable 'use_recycle_bin' so that online deletion events from OneDrive are moved to your system Trash rather than deleted from your local disk.\n\nIf in doubt, stop now and back up your local data before continuing.\n\nAre you sure you wish to proceed with --resync? [Y/N]\n```\n\nYou must press `Y` or `y` to continue with `--resync` action. Any other entry will exit the application.\n\n#### Understanding the --resync risks and behaviour\nA `--resync` **does not delete local-only files**. When a file exists locally but not in OneDrive, and is not excluded via a `sync_list` rule, it is treated as **new local content** and will be uploaded during the resynchronisation process.\n\nLocal deletion of such files when using `--resync` only occurs when using the explicit local data destructive modes such as:\n```text\n--download-only --cleanup-local-files\n```\n\nThe risks associated with `--resync` stem entirely from the loss of the local historic state:\n* The client no longer knows which side previously held the authoritative version of your data.\n* Conflict handling still protects data using safe-backup mechanisms, but may result in renamed or duplicated files.\n* Upload and download volumes may spike significantly.\n* Increased calls to the Microsoft Graph API may result in temporary throttling (HTTP 429 responses).\n\nThis makes it essential that users **verify actions with `--dry-run`** and **maintain proper backups**.\n\n#### Best-practice guidance when using --resync\n\n1. Always back up your data. This client is **not** a backup system. Ensure your `sync_dir` is protected with real backup tooling such as:\n    - rsnapshot\n\t- borg\n\t- restic\n\t- Timeshift\n\t- ZFS or Btrfs snapshots\n\n2. Use `--dry-run` before a real `--resync`\n\n   Allows you to preview all intended changes without modifying your filesystem.\n\n3. Enable the Recycle Bin feature\n\n   Set `use_recycle_bin = \"true\"` in your application configuration. When enabled:\n    - Online deletions received from OneDrive via the Graph API are moved to the FreeDesktop.org-compliant system Trash rather than being permanently deleted from your disk.\n\n4. Avoid using `--resync` unnecessarily\n\n   Only use it:\n    - When the client explicitly requests it, or\n\t- When you’ve confirmed, via logs or sync status, that the local state has become invalid\n\t\n> [!CAUTION]\n> Avoid configuring `--resync` as a default startup option.\n\n#### Automated environments\nIf you **fully understand the implications** and are operating in a scripted or automated environment, you may bypass the confirmation prompt by adding:\n\n```bash\n--resync-auth\n```\n\nThis should **only** be used when automation requires non-interactive operation and robust backups are in place.\n\n\n\n### Performing a --force-sync without a --resync or changing your configuration\nIn 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.\n\nThe `--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`.\n\nTo use this option, you must run the application manually in the following manner:\n```text\nonedrive --sync --single-directory '<directory_to_sync>' --force-sync <add any other options needed or required>\n```\n\nWhen using `--force-sync`, you'll encounter the following warning and advice:\n```text\nWARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --sync --single-directory --force-sync being used\n\nUsing --force-sync will reconfigure the application to use defaults. This may have unknown future impacts.\nBy proceeding with this option, you accept any impacts, including potential data loss resulting from using --force-sync.\n\nAre you sure you want to proceed with --force-sync [Y/N] \n```\n\nTo proceed with `--force-sync`, you must type 'y' or 'Y' to allow the application to continue.\n\n### Enabling the Client Activity Log\nWhen 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.\n\nBy 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.\n\n> [!NOTE]\n> 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:\n> ```text\n> ERROR: Unable to access /var/log/onedrive\n> ERROR: Please manually create '/var/log/onedrive' and set appropriate permissions to allow write access\n> ERROR: The requested client activity log will instead be located in your user's home directory\n> ```\n\nOn many systems, ensuring that the log directory exists can be achieved by performing the following:\n```text\nsudo mkdir /var/log/onedrive\nsudo chown root:users /var/log/onedrive\nsudo chmod 0775 /var/log/onedrive\n```\n\nAdditionally, you need to ensure that your user account is part of the 'users' group:\n```\ncat /etc/group | grep users\n```\n\nIf your user is not part of this group, then you need to add your user to this group:\n```\nsudo usermod -a -G users <your-user-name>\n```\n\nIf 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.\n\nIf the client is unable to write the client activity log, the following error message will be printed:\n```text\nERROR: Unable to write the activity log to /var/log/onedrive/%username%.onedrive.log\nERROR: Please set appropriate permissions to allow write access to the logging directory for your user account\nERROR: The requested client activity log will instead be located in your user's home directory\n```\n\nIf you receive this error message, you will need to diagnose why your system cannot write to the specified file location.\n\n#### Client Activity Log Example:\nAn example of a client activity log for the command `onedrive --sync --enable-logging` is below:\n```text\n2023-Sep-27 08:16:00.1128806    Configuring Global Azure AD Endpoints\n2023-Sep-27 08:16:00.1160620    Sync Engine Initialised with new Onedrive API instance\n2023-Sep-27 08:16:00.5227122    All application operations will be performed in: /home/user/OneDrive\n2023-Sep-27 08:16:00.5227977    Fetching items from the OneDrive API for Drive ID: <redacted>\n2023-Sep-27 08:16:00.7780979    Processing changes and items received from Microsoft OneDrive ...\n2023-Sep-27 08:16:00.7781548    Performing a database consistency and integrity check on locally stored data ... \n2023-Sep-27 08:16:00.7785889    Scanning the local file system '~/OneDrive' for new data to upload ...\n2023-Sep-27 08:16:00.7813710    Performing a final true-up scan of online data from Microsoft OneDrive\n2023-Sep-27 08:16:00.7814668    Fetching items from the OneDrive API for Drive ID: <redacted>\n2023-Sep-27 08:16:01.0141776    Processing changes and items received from Microsoft OneDrive ...\n2023-Sep-27 08:16:01.0142454    Sync with Microsoft OneDrive is complete\n```\nAn example of a client activity log for the command `onedrive --sync --verbose --enable-logging` is below:\n```text\n2023-Sep-27 08:20:05.4600464    Checking Application Version ...\n2023-Sep-27 08:20:05.5235017    Attempting to initialise the OneDrive API ...\n2023-Sep-27 08:20:05.5237207    Configuring Global Azure AD Endpoints\n2023-Sep-27 08:20:05.5238087    The OneDrive API was initialised successfully\n2023-Sep-27 08:20:05.5238536    Opening the item database ...\n2023-Sep-27 08:20:05.5270612    Sync Engine Initialised with new Onedrive API instance\n2023-Sep-27 08:20:05.9226535    Application version:  vX.Y.Z-A-bcdefghi\n2023-Sep-27 08:20:05.9227079    Account Type:         <account-type>\n2023-Sep-27 08:20:05.9227360    Default Drive ID:     <redacted>\n2023-Sep-27 08:20:05.9227550    Default Root ID:      <redacted>\n2023-Sep-27 08:20:05.9227862    Remaining Free Space: <space-available>\n2023-Sep-27 08:20:05.9228296    All application operations will be performed in: /home/user/OneDrive\n2023-Sep-27 08:20:05.9228989    Fetching items from the OneDrive API for Drive ID: <redacted>\n2023-Sep-27 08:20:06.2076569    Performing a database consistency and integrity check on locally stored data ... \n2023-Sep-27 08:20:06.2077121    Processing DB entries for this Drive ID: <redacted>\n2023-Sep-27 08:20:06.2078408    Processing ~/OneDrive\n2023-Sep-27 08:20:06.2078739    The directory has not changed\n2023-Sep-27 08:20:06.2079783    Processing Attachments\n2023-Sep-27 08:20:06.2080071    The directory has not changed\n2023-Sep-27 08:20:06.2081585    Processing Attachments/file.docx\n2023-Sep-27 08:20:06.2082079    The file has not changed\n2023-Sep-27 08:20:06.2082760    Processing Documents\n2023-Sep-27 08:20:06.2083225    The directory has not changed\n2023-Sep-27 08:20:06.2084284    Processing Documents/file.log\n2023-Sep-27 08:20:06.2084886    The file has not changed\n2023-Sep-27 08:20:06.2085150    Scanning the local file system '~/OneDrive' for new data to upload ...\n2023-Sep-27 08:20:06.2087133    Skipping item - excluded by sync_list config: ./random_25k_files\n2023-Sep-27 08:20:06.2116235    Performing a final true-up scan of online data from Microsoft OneDrive\n2023-Sep-27 08:20:06.2117190    Fetching items from the OneDrive API for Drive ID: <redacted>\n2023-Sep-27 08:20:06.5049743    Sync with Microsoft OneDrive is complete\n```\n\n#### Client Activity Log Differences\nDespite 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:\n\n**No user configuration file:**\n```text\nNo user or system config file found, using application defaults\nUsing 'user' configuration path for application state data: /home/user/.config/onedrive\nUsing the following path to store the runtime application log: /var/log/onedrive\n```\n**User configuration file:**\n```text\nReading configuration file: /home/user/.config/onedrive/config\nConfiguration file successfully loaded\nUsing 'user' configuration path for application state data: /home/user/.config/onedrive\nUsing the following path to store the runtime application log: /var/log/onedrive\n```\n\n### Display Manager Integration\nModern desktop environments such as GNOME and KDE Plasma provide graphical file managers — Nautilus (GNOME Files) and Dolphin, respectively — to help users navigate their local and remote storage.\n\n#### What “Display Manager Integration” means\nDisplay Manager Integration refers to an ability to integrate your configured Microsoft OneDrive synchronisation directory (`sync_dir`) with the desktop’s file manager environment. Depending on the platform and desktop environment, this may include:\n\n1. **Sidebar registration** — Adding the OneDrive folder as a “special place” within the sidebar of Nautilus (GNOME) or Dolphin (KDE), providing easy access without manual navigation.\n2. **Custom folder icon** — Applying a dedicated OneDrive icon to visually distinguish the synchronised directory within the file manager.\n3. **Context-menu extensions** — Adding right-click actions such as “Upload to OneDrive” or “Share via OneDrive” directly inside Nautilus or Dolphin.\n4. **File overlay badges** — Displaying icons (check-marks, sync arrows, or cloud symbols) to represent file synchronisation state.\n5. **System tray or application indicator** — Presenting sync status, pause/resume controls, or notifications via a tray icon.\n\n#### What display manager integration is available in the OneDrive Client for Linux\nThe OneDrive Client for Linux currently supports the following integration features:\n\n1. **Sidebar registration** — The client automatically registers the OneDrive folder as a “special place” within the sidebar of Nautilus (GNOME) or Dolphin (KDE).\n2. **Custom folder icon** — The client applies a OneDrive-specific icon to the synchronisation directory where supported by the installed icon theme.\n\nSidebar registration and custom folder icon behaviour is controlled by the configuration option:\n```text\ndisplay_manager_integration = \"true\"\n```\nWhen enabled, the client detects the active desktop session and applies the corresponding integration automatically when the client is running in `--monitor` mode only.\n\n> [!NOTE] \n> Display Manager Integration remains active only while the OneDrive client or its systemd service is running. If the client stops or the service is stopped, the desktop integration is automatically cleared. It is re-applied the next time the client starts.\n\n#### Fedora (GNOME) Display Manager Integration Example\n![fedora_integration](./images/fedora_integration.png)\n\n#### Fedora (KDE) Display Manager Integration Example\n![fedora_kde_integration](./images/fedora_kde_integration.png)\n\n#### Ubuntu Display Manager Integration Example\n![ubuntu_integration](./images/ubuntu_integration.png)\n\n#### Kubuntu Display Manager Integration Example\n![kubuntu_integration](./images/kubuntu_integration.png)\n\n\nAdditionally, the following display manager integrations are independent from the above configuration specification:\n\n1. **GUI Notifications** — The client (when compiled with `--enable-notifications`) will send notifications to the GUI when important events occur.\n2. **Recycle Bin** — When `use_recycle_bin = \"true\"` is enabled, the client uses the FreeDesktop.org Trash Specification–compliant recycle bin for any online deletions that are processed locally. This capability can be utilised even when no GUI is available.\n\n\n#### What about context menu integration?\nContext-menu integration is a desktop-specific capability, not part of the core OneDrive Client. It can be achieved through desktop-provided extension mechanisms:\n\n1. **Shell-script bridge** — A simple shell script can be registered as a KDE ServiceMenu or a GNOME Nautilus Script to trigger local actions (for example, creating a symbolic link in `~/OneDrive` to upload a file).\n2. **Python + Nautilus API (GNOME)** — Implemented via nautilus-python bindings by registering a subclass of `Nautilus.MenuProvider`.\n3. **Qt/KIO Plugins (KDE)** — Implemented using C++ or declarative .desktop ServiceMenu definitions under `/usr/share/kservices5/ServiceMenus/`.\n\nThese methods are optional and operate independently of the core OneDrive Client. They can be used by advanced users or system integrators to provide additional right-click functionality.\n\n#### What about file overlay badges?\nFile overlay badges are typically associated with Microsoft’s Files-On-Demand feature, which allows selective file downloads and visual state indicators (online-only, available offline, etc.).\n\nBecause Files-On-Demand is currently a feature request for this client, overlay badges are not implemented and remain out of scope for now.\n\n#### What about a system tray or application indicator?\nWhile the core OneDrive Client for Linux does not include its own tray icon or GUI dashboard, the community provides complementary tools that plug into it — exposing sync status, pause/resume controls, tray menus, and GUI configuration front-ends. Below are two popular options:\n\n**1. OneDriveGUI** - https://github.com/bpozdena/OneDriveGUI\n* A full-featured graphical user interface built for the OneDrive Linux client.\n* Key features include: multi-account support, asynchronous real-time monitoring of multiple OneDrive profiles, a setup wizard for profile creation/import, automatic sync on GUI startup, and GUI-based login. \n* Includes tray icon support when the desktop environment allows it. \n* Intended to simplify one-click configuration of the CLI client, help users visualise current operations (uploads/downloads), and manage advanced features such as SharePoint libraries and multiple profiles.\n\n**2. onedrive_tray** - https://github.com/DanielBorgesOliveira/onedrive_tray\n* A lightweight system tray utility written in Qt (using libqt5 or later) that monitors the running OneDrive Linux client and displays status via a tray icon. \n* Left-click the tray icon to view sync progress; right-click to access a menu of available actions; middle-click shows the PID of the running client. \n* Ideal for users who just want visual status cues (e.g., “sync in progress”, “idle”, “error”) without a full GUI configuration tool.\n\n### GUI Notifications\nTo 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:\n\n* A D-Bus message bus daemon must be running.\n* The environment variables XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS must be set.\n\nWithout these conditions met, GUI notifications will not function even if the support is compiled in.\n\nOnce these conditions have been met, the following application events will trigger a GUI notification within the display manager session by default:\n*   Aborting a sync if .nosync file is found\n*   Skipping a particular item due to an invalid name\n*   Skipping a particular item due to an invalid symbolic link\n*   Skipping a particular item due to an invalid UTF sequence\n*   Skipping a particular item due to an invalid character encoding sequence\n*   Cannot create remote directory\n*   Cannot upload file changes (free space issue, breaches maximum allowed size, breaches maximum OneDrive Account path length)\n*   Cannot delete remote file / folder\n*   Cannot move remote file / folder\n*   When a re-authentication is required\n*   When a new client version is available\n*   Files that fail to upload\n*   Files that fail to download\n\nAdditionally, GUI notifications can also be sent for the following activities:\n*   Successful file download\n*   Successful file upload\n*   Successful deletion locally (files and folders)\n*   Successful deletion online (files and folders)\n\nTo enable these specific notifications, add the following to your 'config' file:\n```\nnotify_file_actions = \"true\"\n```\n\nTo disable *all* GUI notifications, add the following to your 'config' file:\n```\ndisable_notifications = \"true\"\n```\n\n### Using a local Recycle Bin\nBy default, this application will process online deletions and directly delete the corresponding file or folder directly from your configured 'sync_dir'.\n\nIn some cases, it may actually be desirable to move these files to your Linux user default 'Recycle Bin', so that you can manually delete the files at your own discretion.\n\nTo enable this application functionality, add the following to your 'config' file:\n```\nuse_recycle_bin = \"true\"\n```\n\nThis capability is designed to be compatible with the [FreeDesktop.org Trash Specification](https://specifications.freedesktop.org/trash/latest/), ensuring interoperability with GUI-based desktop environments such as GNOME (GIO) and KDE (KIO). It follows the required structure by:\n* Moving deleted files and directories to `~/.local/share/Trash/files/`\n* Creating matching metadata files in `~/.local/share/Trash/info/` with the correct `.trashinfo` format, including the original absolute path and ISO 8601-formatted deletion timestamp\n* Resolving filename collisions using a `name.N.ext` pattern (e.g., `Document.2.docx`), consistent with GNOME and KDE behaviour.\n\nTo specify an explicit 'Recycle Bin' directory, add the following to your 'config' file:\n```\nrecycle_bin_path = \"/path/to/desired/location/\"\n```\n\nThe same FreeDesktop.org Trash Specification will be used with this explicit 'Recycle Bin' directory as illustrated below:\n\n![using_recycle_bin](./images/using_recycle_bin.png)\n\n\n### Handling a Microsoft OneDrive Account Password Change\nIf 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:\n```text\nAADSTS50173: 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 '<date-and-timestamp>' and the TokensValidFrom date (before which tokens are not valid) for this user is '<date-and-timestamp>'.\n\nERROR: You will need to issue a --reauth and re-authorise this client to obtain a fresh auth token.\n```\n\nTo re-authorise the client, follow the steps below:\n1.   If running the client as a system service (init.d or systemd), stop the applicable system service\n2.   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.\n3.   Restart the client if running as a system service or perform the standalone sync operation again\n\nThe application will now sync with OneDrive with the new credentials.\n\n### Determining the synchronisation result\nWhen the client has finished syncing without errors, the following will be displayed:\n```\nSync with Microsoft OneDrive is complete\n```\n\nIf any items failed to sync, the following will be displayed:\n```\nSync with Microsoft OneDrive has completed, however there are items that failed to sync.\n```\nA file list of failed upload or download items will also be listed to allow you to determine your next steps.\n\nIn order to fix the upload or download failures, you may need to:\n*   Review the application output to determine what happened\n*   Re-try your command utilising a resync to ensure your system is correctly synced with your Microsoft OneDrive Account\n\n### Resumable Transfers\nThe OneDrive Client for Linux supports resumable transfers for both uploads and downloads. This capability enhances the reliability and robustness of file transfers by allowing interrupted operations to continue from the last successful point, instead of restarting from the beginning. This is especially important in environments with unstable network connections or during large file transfers.\n\n#### What Are Resumable Transfers?\nA resumable transfer is a process that:\n*   Detects when a file upload or download was interrupted due to a network error, system shutdown, or other external factors.\n*   Saves the current state of the transfer, including offsets, temporary filenames, and online session metadata.\n*   Upon application restart, automatically detects these incomplete operations and resumes them from where they left off.\n\n#### When Does It Occur?\nResumable transfers are automatically engaged when:\n*   The application is not started with `--resync`.\n*   Interrupted downloads exist with associated metadata saved to disk.\n*   Interrupted uploads using session-based transfers are pending resumption.\n\n> [!IMPORTANT]\n> If a `--resync` operation is being performed, all resumable transfer metadata is purged to ensure a clean and consistent resynchronisation state.\n\n#### How It Works Internally\n*   **Downloads:** Partial download state is stored as a JSON metadata file, including the online hash, download URL, and byte offset. The file itself is saved with a `.partial` suffix. When detected, this metadata is parsed and the download resumes using HTTP range headers.\n*   **Uploads:** Session uploads use OneDrive Upload Sessions. If interrupted, the session URL and transfer state are persisted. On restart, the client attempts to resume the upload using the remaining byte ranges.\n\n#### Benefits of Resumable Transfers\n*   Saves bandwidth by avoiding full re-transfer of large files.\n*   Improves reliability in poor network conditions.\n*   Increases performance and reduces recovery time after unexpected shutdowns.\n\n#### Considerations\nResumable state is only preserved if the client exits gracefully or the system preserves temporary files across sessions.\n\nIf `--resync` is used, all resumable data is discarded intentionally.\n\n#### Recommendations\n*   Avoid using `--resync` unless explicitly required.\n*   Enable logging (`--enable-logging`) to help diagnose resumable transfer behaviour.\n*   For environments where network interruptions are common, ensure that the system does not clean temporary or cache files between reboots.\n\n> [!NOTE] \n> Resumable transfer support is built-in and requires no special configuration. It is automatically applied during both standalone and monitor operational modes when applicable.\n\n## Frequently Asked Configuration Questions\n\n### How to change the default configuration of the client?\nThe OneDrive Client for Linux determines its configuration from three layers, applied in the following order of priority:\n\n1. Application default values – internal defaults built into the client\n2. Configuration file values – user-defined settings from a config file (if present)\n3. Command-line arguments – values passed at runtime override both of the above\n\nThe built-in application defaults are sufficient for most users and provide a reliable operational baseline. Adding a configuration file or command-line options is optional, and only required when you want to customise application runtime behaviour.\n\n>[!NOTE]\n> The OneDrive Client does not create a configuration file automatically.\n> If no configuration file is found, the client runs entirely using its internally defined default values.\n> You only need to create a config file if you wish to override those defaults.\n\nIf you want to adjust the default settings, download a copy of the configuration template into your local configuration directory. Valid configuration file locations are:\n*   `~/.config/onedrive` – for per-user configuration\n*   `/etc/onedrive` – for system-wide configuration\n\n> [!TIP] \n> To download a copy of the default configuration template, run:\n> ```text\n> mkdir -p ~/.config/onedrive\n> wget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/onedrive/config\n> ```\n\nFor a full list of configuration options and command-line switches, see [application-config-options.md](application-config-options.md)\n\n### How to change where my data from Microsoft OneDrive is stored?\nBy 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.\n\nTo 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.\n\n> [!IMPORTANT]\n> 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.\n\n### Why does the client create 'safeBackup' files?\n'safeBackup' files are created to prevent local data loss whenever the client is about to replace or remove a local file and there’s any chance the current on-disk content might be different to what OneDrive expects.\n\nUnder the hood, the client makes specific decisions right before a local file would otherwise be overwritten, renamed, or deleted. Instead of risking silent data loss, the client renames your current local file to a clearly marked backup name and then proceeds with the sync action.\n\nFrom v2.5.3+, the backup name is:\n```\nfilename-hostname-safeBackup-0001.ext\n```\nThe client will increment the number if additional backups are needed.\n\n#### The most common reasons you’ll see 'safeBackup' files\n**1. You ran the client with `--resync`**\n\n`--resync` intentionally discards the client’s local state, so the client no longer “knows” what used to be in sync. During the first pass after a resync, the online state is treated as source-of-truth. If the client finds a local file whose content differs from the online version (hash mismatch), it will back up your local copy first and then bring the local file in line with OneDrive.\n\nIf you wish to treat your local files as the source-of-truth, you can set the following configuration option:\n```\nlocal_first = \"true\"\n```\n\n**2. Dual-booting and pointing sync_dir at your Windows OneDrive folder.**\n\nIf you dual boot and set the Linux client’s sync_dir to the same path used by the Windows client, there will be times when files already exist on disk without matching local DB entries or with content that changed while Linux wasn’t running. When the Linux client encounters such a file (e.g. “exists locally but isn’t represented the way the DB expects” or “exists but content/hash differs”), the client will protect the on-disk content by creating a 'safeBackup' before it reconciles the file.\n\n**3. The online file was modified (server-side) and now differs from your local copy**\n\nIf Microsoft OneDrive (or another app) changes a file online, the hash reported by the Graph API won’t match your local content. When the client is about to update the local item to match what’s online, a 'safeBackup' is created so your current local data isn’t lost if the client determines that this action should be taken.\n\n#### Can I turn this functionality off?\n\nYes, but be careful. To disable local data protection entirely, set the following configuration option:\n```\nbypass_data_preservation = \"true\"\n```\nIf you enable this, the client will not create 'safeBackup' files and may overwrite or remove local content during conflict resolution. **Use with extreme caution.**\n\nIf you simply don’t want 'safeBackup' files uploaded to OneDrive, it is advisable to keep protection enabled and add a 'skip_file' rule:\n```\nskip_file = \"~*|.~*|*.tmp|*.swp|*.partial|*-safeBackup-*\"\n```\nThis allows you to handle the safeBackup files locally, without having to remediate anything online.\n\n### How to change what file and directory permissions are assigned to data that is downloaded from Microsoft OneDrive?\nThe following are the application default permissions for any new directory or file that is created locally when downloaded from Microsoft OneDrive:\n*   Directories: 700 - This provides the following permissions: `drwx------`\n*   Files: 600 - This provides the following permissions: `-rw-------`\n\nThese 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.\n\nTo 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.\n```text\nsync_dir_permissions = \"700\"\nsync_file_permissions = \"600\"\n```\n\n> [!IMPORTANT]\n> 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.\n\n> [!NOTE]\n> 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.\n\n### How are uploads and downloads managed?\nThe 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. Refer to [configuration documentation](application-config-options.md#threads) for further details.\n\n### How to only sync a specific directory?\nThere are two methods to achieve this:\n*   Employ the '--single-directory' option to only sync this specific path\n*   Employ 'sync_list' as part of your 'config' file to configure what files and directories to sync, and what should be excluded\n\n### How to 'skip' files from syncing?\nThere are two methods to achieve this:\n*   Employ 'skip_file' as part of your 'config' file to configure what files to skip\n*   Employ 'sync_list' to configure what files and directories to sync, and what should be excluded\n\nFor further details please read the ['skip_file' config option documentation](https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md#skip_file)\n\n### How to 'skip' directories from syncing?\nThere are three methods available to 'skip' a directory from the sync process:\n*   Employ 'skip_dir' as part of your 'config' file to configure what directories to skip\n*   Employ 'sync_list' to configure what files and directories to sync, and what should be excluded\n*   Employ 'check_nosync' as part of your 'config' file and a '.nosync' empty file within the directory to exclude to skip that directory\n\n> [!IMPORTANT]\n> Entries for 'skip_dir' are *relative* to your 'sync_dir' path.\n\nFor further details please read the ['skip_dir' config option documentation](https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md#skip_dir)\n\n### How to 'skip' .files and .folders from syncing?\nThere are three methods to achieve this:\n*   Employ 'skip_file' or 'skip_dir' to configure what files or folders to skip\n*   Employ 'sync_list' to configure what files and directories to sync, and what should be excluded\n*   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\n\n### How to 'skip' files larger than a certain size from syncing?\nUse `skip_size = \"value\"` as part of your 'config' file where files larger than this size (in MB) will be skipped.\n\n### How to 'rate limit' the application to control bandwidth consumed for upload & download operations?\nTo 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.\n\nBy default, 'rate_limit' is set to '0', indicating that the application will utilise the maximum available bandwidth across all threads.\n\nTo check the current 'rate_limit' value, use the `--display-config` command.\n\n> [!NOTE]\n> 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.\n\n### How can I prevent my local disk from filling up?\nBy default, the application will reserve 50MB of disk space to prevent your filesystem from running out of disk space.\n\nThis default value can be modified by adding the 'space_reservation' configuration option and the applicable value as part of your 'config' file.\n\nYou can review the value being used when using `--display-config`.\n\n### How does the client handle symbolic links?\nMicrosoft 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.\n\nAs such, there are only two methods to support symbolic links with this client:\n1. Follow the Linux symbolic link and upload whatever the local symbolic link is pointing to to Microsoft OneDrive. This is the default behaviour.\n2. Skip symbolic links by configuring the application to do so. When skipping, no data, no link, no reference is uploaded to OneDrive.\n\nUse 'skip_symlinks' as part of your 'config' file to configure the skipping of all symbolic links while syncing.\n\n### How to synchronise OneDrive Personal Shared Folders?\nFolders 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\".\n\n### How to synchronise OneDrive Business Shared Items (Files and Folders)?\nFolders 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\".\n\nFiles shared with you can be synchronised using two methods:\n1. Add a shortcut link to the file to your OneDrive folder online\n2. Sync the actual file locally using the configuration option to sync OneDrive Business Shared Files.\n\nRefer to [business-shared-items.md](business-shared-items.md) for further details.\n\n### How to synchronise SharePoint / Office 365 Shared Libraries?\nThere are two methods to achieve this:\n* 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\".\n* Configure a separate application instance to only synchronise that specific SharePoint Library. Refer to [sharepoint-libraries.md](sharepoint-libraries.md) for configuration assistance.\n\n### How to Create a Shareable Link?\nIn certain situations, you might want to generate a shareable file link and provide this link to other users for accessing a specific file.\n\nTo accomplish this, employ the following command:\n```text\nonedrive --create-share-link <path/to/file>\n```\n> [!IMPORTANT]\n> By default, this access permissions for the file link will be read-only.\n\nTo make the shareable link a read-write link, execute the following command:\n```text\nonedrive --create-share-link <path/to/file> --with-editing-perms\n```\n> [!IMPORTANT]\n> The order of the file path and option flag is crucial.\n\n### How to Synchronise Both Personal and Business Accounts at once?\nYou need to set up separate instances of the application configuration for each account.\n\nRefer to [advanced-usage.md](advanced-usage.md) for guidance on configuration.\n\n### How to Synchronise Multiple SharePoint Libraries simultaneously?\nFor each SharePoint Library, configure a separate instance of the application configuration.\n\nRefer to [advanced-usage.md](advanced-usage.md) for configuration instructions.\n\n### How to Receive Real-time Changes from Microsoft OneDrive Service, instead of waiting for the next sync period?\n\nRefer to [webhooks.md](webhooks.md) for configuration instructions.\n\n### How to initiate the client as a background service?\nThere are a few ways to employ onedrive as a service:\n* via init.d\n* via systemd\n* via runit\n\n#### OneDrive service running as root user via init.d\n```text\nchkconfig onedrive on\nservice onedrive start\n```\nTo view the logs, execute:\n```text\ntail -f /var/log/onedrive/<username>.onedrive.log\n```\nTo 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.\n\n#### OneDrive service running as root user via systemd (Arch, Ubuntu, Debian, OpenSuSE, Fedora)\nInitially, switch to the root user with `su - root`, then activate the systemd service:\n```text\nsystemctl --user enable onedrive\nsystemctl --user start onedrive\n```\n\n> [!IMPORTANT]\n> This will execute the 'onedrive' process with a UID/GID of '0', which means any files or folders created will be owned by 'root'.\n\n> [!IMPORTANT]\n> The `systemctl --user` command is not applicable to Red Hat Enterprise Linux (RHEL) or CentOS Linux platforms - see below.\n\nTo monitor the service's status, use the following:\n```text\nsystemctl --user status onedrive.service\n```\n\nTo observe the systemd application logs, use:\n```text\njournalctl --user-unit=onedrive -f\n```\n\n> [!TIP]\n> For systemd to function correctly, it requires the presence of XDG environment variables. If you encounter the following error while enabling the systemd service:\n> ```text\n> Failed to connect to bus: No such file or directory\n> ```\n> 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:\n> ```text\n> export XDG_RUNTIME_DIR=\"/run/user/$UID\"\n> export DBUS_SESSION_BUS_ADDRESS=\"unix:path=${XDG_RUNTIME_DIR}/bus\"\n> ```\n> \n> To apply this change, you must log out of all user accounts where it has been made.\n\n> [!IMPORTANT]\n> 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:\n> 1. Create a symbolic link from `/home/root/.config/onedrive` to `/root/.config/onedrive/`.\n> 2. Establish a systemd service using the '@' service file: `systemctl enable onedrive@root.service`.\n> 3. Start the root@service: `systemctl start onedrive@root.service`.\n>\n> This ensures that the service correctly restarts upon system reboot.\n\nTo examine the systemd application logs, run:\n```text\njournalctl --unit=onedrive@<username> -f\n```\n\n#### OneDrive service running as root user via systemd (Red Hat Enterprise Linux, CentOS Linux)\n```text\nsystemctl enable onedrive\nsystemctl start onedrive\n```\n> [!IMPORTANT]\n> This will execute the 'onedrive' process with a UID/GID of '0', meaning any files or folders created will be owned by 'root'.\n\nTo view the systemd application logs, execute:\n```text\njournalctl --unit=onedrive -f\n```\n\n#### OneDrive service running as a non-root user via systemd (All Linux Distributions)\nIn 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.\n\n1. As the user who will run the service, launch the application in standalone mode, authorise it for use, and verify that synchronisation is functioning as expected:\n```text\nonedrive --sync --verbose\n```\n2. After validating the application for your user, switch to the 'root' user, where <username> is your username from step 1 above.\n```text\nsystemctl enable onedrive@<username>.service\nsystemctl start onedrive@<username>.service\n```\n3. To check the service's status for the user, use the following:\n```text\nsystemctl status onedrive@<username>.service\n```\n\nTo observe the systemd application logs, use:\n```text\njournalctl --unit=onedrive@<username> -f\n```\n\n#### OneDrive service running as a non-root user via systemd (with notifications enabled) (Arch, Ubuntu, Debian, OpenSuSE, Fedora)\nIn some scenarios, you may want to receive GUI notifications when using the client as a non-root user. In this case, follow these steps:\n\n1. Log in via the graphical UI as the user you want to enable the service for.\n2. Disable any `onedrive@` service files for your username, e.g.:\n```text\nsudo systemctl stop onedrive@alex.service\nsudo systemctl disable onedrive@alex.service\n```\n3. Enable the service as follows:\n```text\nsystemctl --user enable onedrive\nsystemctl --user start onedrive\n```\n\nTo check the service's status for the user, use the following:\n```text\nsystemctl --user status onedrive.service\n```\n\nTo view the systemd application logs, execute:\n```text\njournalctl --user-unit=onedrive -f\n```\n\n> [!IMPORTANT]\n> The `systemctl --user` command is not applicable to Red Hat Enterprise Linux (RHEL) or CentOS Linux platforms.\n\n#### OneDrive service running as a non-root user via runit (antiX, Devuan, Artix, Void)\n\n1. Create the following folder if it doesn't already exist: `/etc/sv/runsvdir-<username>`\n\n  - where `<username>` is the `USER` targeted for the service\n  - e.g., `# mkdir /etc/sv/runsvdir-nolan`\n\n2. Create a file called `run` under the previously created folder with executable permissions\n\n   - `# touch /etc/sv/runsvdir-<username>/run`\n   - `# chmod 0755 /etc/sv/runsvdir-<username>/run`\n\n3. Edit the `run` file with the following contents (permissions needed):\n\n  ```sh\n  #!/bin/sh\n  export USER=\"<username>\"\n  export HOME=\"/home/<username>\"\n\n  groups=\"$(id -Gn \"${USER}\" | tr ' ' ':')\"\n  svdir=\"${HOME}/service\"\n\n  exec chpst -u \"${USER}:${groups}\" runsvdir \"${svdir}\"\n  ```\n\n  - Ensure you replace `<username>` with the `USER` set in step #1.\n\n4. Enable the previously created folder as a service\n\n  - `# ln -fs /etc/sv/runsvdir-<username> /var/service/`\n\n5. Create a subfolder in the `USER`'s `HOME` directory to store the services (or symlinks)\n\n   - `$ mkdir ~/service`\n\n6. Create a subfolder specifically for OneDrive\n\n  - `$ mkdir ~/service/onedrive/`\n\n7. Create a file called `run` under the previously created folder with executable permissions\n\n   - `$ touch ~/service/onedrive/run`\n   - `$ chmod 0755 ~/service/onedrive/run`\n\n8. Append the following contents to the `run` file\n\n  ```sh\n  #!/usr/bin/env sh\n  exec /usr/bin/onedrive --monitor\n  ```\n\n  - In some scenarios, the path to the `onedrive` binary may vary. You can obtain it by running `$ command -v onedrive`.\n\n9. Reboot to apply the changes\n\n10. Check the status of user-defined services\n\n  - `$ sv status ~/service/*`\n\n> [!NOTE]\n> For additional details, you can refer to Void's documentation on [Per-User Services](https://docs.voidlinux.org/config/services/user-services.html)\n\n### How to start a user systemd service at boot without user login?\nIn some situations, it may be necessary for the systemd service to start without requiring your 'user' to log in.\n\nTo 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:\n```text\nloginctl enable-linger <your_user_name>\n```\n\n### How to access Microsoft OneDrive service through a proxy\nIf you have a requirement to run the client through a proxy, there are a couple of ways to achieve this:\n\n#### Option 1: Use '.bashrc' to specify the proxy server details\nSet proxy configuration in `~/.bashrc` to allow the 'onedrive' application to use a specific proxy server:\n```text\n# Set the HTTP proxy\nexport http_proxy=\"http://your.proxy.server:port\"\n\n# Set the HTTPS proxy\nexport https_proxy=\"http://your.proxy.server:port\"\n```\n\nOnce you've edited your `~/.bashrc` file, run the following command to apply the changes:\n```\nsource ~/.bashrc\n```\n\n#### Option 2: Update the 'systemd' service file to include the proxy server details\nIf running as a systemd service, edit the applicable systemd service file to include the proxy configuration information:\n```text\n[Unit]\nDescription=OneDrive Client for Linux\nDocumentation=https://github.com/abraunegg/onedrive\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\n........\nEnvironment=\"HTTP_PROXY=http://your.proxy.server:port\"\nEnvironment=\"HTTPS_PROXY=http://your.proxy.server:port\"\nExecStart=/usr/local/bin/onedrive --monitor\n........\n\n```\n> [!NOTE]\n> 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\n\n### How to set up SELinux for a sync folder outside of the home folder\n\nIf 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`.\n```text\nsudo semanage fcontext -a -t user_home_t /path/to/onedriveSyncFolder\nsudo restorecon -R -v /path/to/onedriveSyncFolder\n```\nTo remove this change from SELinux and restore the default behaviour:\n```text\nsudo semanage fcontext -d /path/to/onedriveSyncFolder\nsudo restorecon -R -v /path/to/onedriveSyncFolder\n```\n\n## Advanced Configuration of the OneDrive Client for Linux\n\nRefer to [advanced-usage.md](advanced-usage.md) for further details on the following topics:\n\n* Configuring the client to use multiple OneDrive accounts / configurations\n* Configuring the client to use multiple OneDrive accounts / configurations using Docker\n* Configuring the client for use in dual-boot (Windows / Linux) situations\n* Configuring the client for use when 'sync_dir' is a mounted directory\n* Upload data from the local ~/OneDrive folder to a specific location on OneDrive\n\n## Overview of all OneDrive Client for Linux CLI Options\nBelow 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.\n\nThe following configuration options are available:\n```text\nonedrive - A client for the Microsoft OneDrive Cloud Service\n\n  Usage:\n    onedrive [options] --sync\n      Do a one-time synchronisation with Microsoft OneDrive\n    onedrive [options] --monitor\n      Monitor filesystem and synchronise regularly with Microsoft OneDrive\n    onedrive [options] --display-config\n      Display the currently used configuration\n    onedrive [options] --display-sync-status\n      Query OneDrive service and report on pending changes\n    onedrive -h | --help\n      Show this help screen\n    onedrive --version\n      Show version\n\n  Options:\n\n  --auth-files '<path or required value>'\n      Perform authentication via files rather than an interactive dialogue. The application reads/writes the required values from/to the specified files\n  --auth-response '<path or required value>'\n      Perform authentication via a supplied response URL rather than an interactive dialogue\n  --check-for-nomount\n      Check for the presence of .nosync in the syncdir root. If found, do not perform sync\n  --check-for-nosync\n      Check for the presence of .nosync in each directory. If found, skip directory from sync\n  --classify-as-big-delete '<path or required value>'\n      Number of children in a path that is locally removed which will be classified as a 'big data delete'\n  --cleanup-local-files\n      Clean up additional local files when using --download-only. This will remove local data\n  --confdir '<path or required value>'\n      Set the directory used to store the configuration files\n  --create-directory '<path or required value>'\n      Create a directory on OneDrive. No synchronisation will be performed\n  --create-share-link '<path or required value>'\n      Create a shareable link for an existing file on OneDrive\n  --debug-https\n      Debug OneDrive HTTPS communication.\n  --destination-directory '<path or required value>'\n      Destination directory for renamed or moved items on OneDrive. No synchronisation will be performed\n  --disable-download-validation\n      Disable download validation when downloading from OneDrive\n  --disable-notifications\n      Do not use desktop notifications in monitor mode\n  --disable-upload-validation\n      Disable upload validation when uploading to OneDrive\n  --display-config\n      Display what options the client will use as currently configured. No synchronisation will be performed\n  --display-quota\n      Display the quota status of the client. No synchronisation will be performed\n  --display-running-config\n      Display what options the client has been configured to use on application startup\n  --display-sync-status\n      Display the sync status of the client. No synchronisation will be performed\n  --download-file '<path or required value>'\n      Download a single file from Microsoft OneDrive\n  --download-only\n      Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive\n  --dry-run\n      Perform a trial sync with no changes made\n  --enable-logging\n      Enable client activity to a separate log file\n  --file-fragment-size\n      Specify the file fragment size for large file uploads (in MB)\n  --force\n      Force the deletion of data when a 'big delete' is detected\n  --force-http-11\n      Force the use of HTTP 1.1 for all operations\n  --force-sync\n      Force a synchronisation of a specific folder, only when using --sync --single-directory and ignore all non-default skip_dir and skip_file rules\n  --get-O365-drive-id '<path or required value>'\n      Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library (DEPRECATED)\n  --get-file-link '<path or required value>'\n      Display the file link of a synced file\n  --get-sharepoint-drive-id '<path or required value>'\n      Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library\n  --help -h\n      This help information.\n  --list-shared-items\n      List OneDrive Business Shared Items\n  --local-first\n      Synchronise from the local directory source first, before downloading changes from OneDrive\n  --log-dir '<path or required value>'\n      Directory where logging output is saved to, needs to end with a slash\n  --logout\n      Log out the current user\n  --modified-by '<path or required value>'\n      Display the last modified by details of a given path\n  --monitor -m\n      Keep monitoring for local and remote changes\n  --monitor-fullscan-frequency '<path or required value>'\n      Number of sync runs before performing a full local scan of the synced directory\n  --monitor-interval '<path or required value>'\n      Number of seconds by which each sync operation is undertaken when idle under monitor mode\n  --monitor-log-frequency '<path or required value>'\n      Frequency of logging in monitor mode\n  --no-remote-delete\n      Do not delete local file 'deletes' from OneDrive when using --upload-only\n  --print-access-token\n      Print the access token, useful for debugging\n  --reauth\n      Reauthenticate the client with OneDrive\n  --remove-directory '<path or required value>'\n      Remove a directory on OneDrive. No synchronisation will be performed\n  --remove-source-files\n      Remove source file after successful transfer to OneDrive when using --upload-only\n  --remove-source-folders\n      Remove the local directory structure post successful file transfer to Microsoft OneDrive when using --upload-only --remove-source-files\n  --resync\n      Forget the last saved state, perform a full sync\n  --resync-auth\n      Approve the use of performing a --resync action\n  --share-password '<path or required value>'\n      Require a password to access the shared link when used with --create-share-link <file>\n  --single-directory '<path or required value>'\n      Specify a single local directory within the OneDrive root to sync\n  --skip-dir '<path or required value>'\n      Skip any directories that match this pattern from syncing\n  --skip-dir-strict-match\n      When matching skip_dir directories, only match explicit matches\n  --skip-dot-files\n      Skip dot files and folders from syncing\n  --skip-file '<path or required value>'\n      Skip any files that match this pattern from syncing\n  --skip-size '<path or required value>'\n      Skip new files larger than this size (in MB)\n  --skip-symlinks\n      Skip syncing of symlinks\n  --source-directory '<path or required value>'\n      Source directory to rename or move on OneDrive. No synchronisation will be performed\n  --space-reservation '<path or required value>'\n      The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation\n  --sync -s\n      Perform a synchronisation with Microsoft OneDrive\n  --sync-root-files\n      Sync all files in sync_dir root when using sync_list\n  --sync-shared-files\n      Sync OneDrive Business Shared Files to the local filesystem\n  --syncdir '<path or required value>'\n      Specify the local directory used for synchronisation to OneDrive\n  --synchronize\n      Perform a synchronisation with Microsoft OneDrive (DEPRECATED)\n  --threads\n      Specify a value for the number of worker threads used for parallel upload and download operations\n  --upload-only\n      Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive\n  --verbose -v+\n      Print more details, useful for debugging (repeat for extra debugging)\n  --version\n      Print the version and exit\n  --with-editing-perms\n      Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link <file>\n```\n\nRefer to [application-config-options.md](application-config-options.md) for in-depth details on all application options. \n"
  },
  {
    "path": "docs/webhooks.md",
    "content": "# How to configure receiving real-time changes from Microsoft OneDrive using webhooks\n\nWhen 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.\n\nWith this setup, any remote changes are promptly synchronised to your local file system, eliminating the need to wait for the next scheduled synchronisation cycle.\n\n> [!IMPORTANT]\n> In March 2023, Microsoft updated the webhook notification capability in Microsoft Graph to only allow valid HTTPS URLs as the destination for subscription updates.\n>\n> 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.\n>\n> 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:\n> * No-IP\n> * DynDNS\n> * DuckDNS\n> * Afraid.org\n> * Cloudflare\n> * Google Domains\n> * Dynu\n> * ChangeIP\n>\n> This FQDN will allow you to create a valid HTTPS certificate for your system, which can be used by Microsoft Graph for webhook functionality.\n>\n> Please note that it is beyond the scope of this document to provide guidance on setting up this requirement.\n\nDepending on your environment, a number of steps are required to configure this application functionality. At a very high level these configuration steps are:\n\n1. Application configuration to enable 'webhooks' functionality\n2. Install and configure 'nginx' as a reverse proxy for HTTPS traffic\n3. Install and configure Let's Encrypt 'certbot' to provide a valid HTTPS certificate for your system using your FQDN\n4. Configure your Firewall or Router to forward traffic to your system\n\n> [!NOTE]\n> The configuration steps below were validated on [Fedora 40 Workstation](https://fedoraproject.org/)\n>\n> 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.\n\n### Step 1: Application configuration\n\n#### Enable the 'webhook' application feature\n*  In your 'config' file, set `webhook_enabled = \"true\"` to activate the webhook feature.\n\n#### Configure the public notification URL\n*  In your 'config' file, set `webhook_public_url = \"https://<your.fully.qualified.domain.name>/webhooks/onedrive\"` as the public URL that will receive subscription updates from the Microsoft Graph API platform.\n\n> [!NOTE]\n> This URL will utilise your FQDN and must be resolvable from the Internet. This FQDN will also be used within your 'nginx' configuration.\n\n#### Testing\nAt this point, if you attempt to test 'webhooks', when they are attempted to be initialised, the following error *should* be generated:\n```\nERROR: Microsoft OneDrive API returned an error with the following message:\n  Error Message:    HTTP request returned status code 400 (Bad Request)\n  Error Reason:     Subscription validation request timed out.\n  Error Code:       ValidationError\n  Error Timestamp:  YYYY-MM-DDThh:mm:ss\n  API Request ID:   eb196382-51d7-4411-984a-45a3fda90463\nWill retry creating or renewing subscription in 1 minute\n```\nThis error is 100% normal at this point.\n\n### Step 2: Install and configure 'nginx'\n\n> [!NOTE]\n> Nginx is a web server that can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache.\n\n#### Install and enable 'nginx'\n*  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.\n\n> [!TIP]\n> You may need to enable firewall rules to allow inbound http and https connections on your system:\n> ```\n> sudo firewall-cmd --permanent --add-service=http\n> sudo firewall-cmd --permanent --add-service=https\n> sudo firewall-cmd --reload\n> ```\n\n#### Verify your 'nginx' installation\n* 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/\n\n![nginx_verify_install](./images/nginx_verify_install.png)\n\n#### Configure 'nginx' to receive the subscription update\n*  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\n```\nserver {\n\tlisten 80;\n\tserver_name <your.fully.qualified.domain.name>;\n\tlocation /webhooks/onedrive {\n\t\t# Proxy Options\n\t\tproxy_http_version 1.1;\n\t\tproxy_pass http://127.0.0.1:8888;\n\t}\n}\n```\nThe configuration above will:\n* Create an endpoint listener at `https://<your.fully.qualified.domain.name>/webhooks/onedrive`\n* Proxy the received traffic at this listener to the local listener TCP port\n\n> [!TIP]\n> 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.\n\n*  Test your 'nginx' configuration using `sudo nginx -t` to validate that there are no errors. If any are identified, please correct them.\n*  Once tested, reload your 'nginx' configuration to activate the webhook reverse proxy configuration.\n\n### Step 4: Initial Firewall/Router Configuration\n*  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.\n\n![initial_firewall_config](./images/initial_firewall_config.png)\n\n* A valid configuration will be similar to the above illustration.\n\n### Step 5: Use Let's Encrypt 'certbot' to create a SSL Certificate and deploy to your 'nginx' webhook configuration\n*  Install the Let's Encrypt 'certbot' tool along with the associated python module 'python-certbot-nginx' for your platform\n*  Run the 'certbot' tool on your platform to generate a valid HTTPS certificate for your `<your.fully.qualified.domain.name>` by running `certbot --nginx`. This should *detect* your active `server_name` from your 'nginx' configuration and install the certificate in the correct manner.\n\n*  The resulting 'nginx' configuration will look something like this:\n```\nserver {\n\tserver_name <your.fully.qualified.domain.name>;\n\tlocation /webhooks/onedrive {\n\t\t# Proxy Options\n\t\tproxy_http_version 1.1;\n\t\tproxy_pass http://127.0.0.1:8888;\n\t}\n\n    listen 443 ssl; # managed by Certbot\n    ssl_certificate /etc/letsencrypt/live/<your.fully.qualified.domain.name>/fullchain.pem; # managed by Certbot\n    ssl_certificate_key /etc/letsencrypt/live/<your.fully.qualified.domain.name>/privkey.pem; # managed by Certbot\n    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot\n    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot\n\n}\nserver {\n    if ($host = <your.fully.qualified.domain.name>) {\n        return 301 https://$host$request_uri;\n    } # managed by Certbot\n\n\n\tlisten 80;\n\tserver_name <your.fully.qualified.domain.name>;\n    return 404; # managed by Certbot\n}\n```\n\n*  Test your 'nginx' configuration using `sudo nginx -t` to validate that there are no errors. If any are identified, please correct them.\n*  Once tested, reload your 'nginx' configuration to activate the webhook reverse proxy configuration.\n\n> [!IMPORTANT]\n> 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.\n\n### Step 6: Update 'nginx' to only use TLS 1.2 and TLS 1.3\nTo 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:\n```\n    # Ensure only TLS 1.2 and TLS 1.3 are used\n    ssl_protocols TLSv1.2 TLSv1.3;\n```\nThe resulting 'nginx' configuration will look something like this:\n```\nserver {\n\tserver_name <your.fully.qualified.domain.name>;\n\tlocation /webhooks/onedrive {\n\t\t# Proxy Options\n\t\tproxy_http_version 1.1;\n\t\tproxy_pass http://127.0.0.1:8888;\n\t}\n\n    listen 443 ssl; # managed by Certbot\n    ssl_certificate /etc/letsencrypt/live/<your.fully.qualified.domain.name>/fullchain.pem; # managed by Certbot\n    ssl_certificate_key /etc/letsencrypt/live/<your.fully.qualified.domain.name>/privkey.pem; # managed by Certbot\n    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot\n    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot\n\t\n    # Ensure only TLS 1.2 and TLS 1.3 are used\n    ssl_protocols TLSv1.2 TLSv1.3;\n}\nserver {\n    if ($host = <your.fully.qualified.domain.name>) {\n        return 301 https://$host$request_uri;\n    } # managed by Certbot\n\n\n\tlisten 80;\n\tserver_name <your.fully.qualified.domain.name>;\n    return 404; # managed by Certbot\n}\n```\n*  Test your 'nginx' configuration using `sudo nginx -t` to validate that there are no errors. If any are identified, please correct them.\n*  Once tested, reload your 'nginx' configuration to activate the webhook reverse proxy configuration.\n\nTo validate that the TLS configuration is working, perform the following tests from a different system that is able to resolve your FQDN externally:\n```\ncurl -I -v --tlsv1.2 --tls-max 1.2 https://<your.fully.qualified.domain.name>\ncurl -I -v --tlsv1.3 --tls-max 1.3 https://<your.fully.qualified.domain.name>\n```\nThis should return valid TLS information similar to the following:\n```\n* Rebuilt URL to: https://your.fully.qualified.domain.name/\n*   Trying 123.123.123.123...\n* TCP_NODELAY set\n* Connected to your.fully.qualified.domain.name (123.123.123.123) port 443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*   CAfile: /etc/pki/tls/certs/ca-bundle.crt\n  CApath: none\n* TLSv1.2 (OUT), TLS handshake, Client hello (1):\n* TLSv1.2 (IN), TLS handshake, Server hello (2):\n* TLSv1.2 (IN), TLS handshake, Certificate (11):\n* TLSv1.2 (IN), TLS handshake, Server key exchange (12):\n* TLSv1.2 (IN), TLS handshake, Server finished (14):\n* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):\n* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.2 (OUT), TLS handshake, Finished (20):\n* TLSv1.2 (IN), TLS handshake, Finished (20):\n* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384\n* ALPN, server accepted to use http/1.1\n* Server certificate:\n*  subject: CN=your.fully.qualified.domain.name\n*  start date: Aug 28 07:18:04 2024 GMT\n*  expire date: Nov 26 07:18:03 2024 GMT\n*  subjectAltName: host \"your.fully.qualified.domain.name\" matched cert's \"your.fully.qualified.domain.name\"\n*  issuer: C=US; O=Let's Encrypt; CN=E6\n*  SSL certificate verify ok.\n> HEAD / HTTP/1.1\n> Host: your.fully.qualified.domain.name\n> User-Agent: curl/7.61.1\n> Accept: */*\n> \n< HTTP/1.1 200 OK\nHTTP/1.1 200 OK\n< Server: nginx/1.26.2\nServer: nginx/1.26.2\n< Date: Sat, 31 Aug 2024 22:36:01 GMT\nDate: Sat, 31 Aug 2024 22:36:01 GMT\n< Content-Type: text/html\nContent-Type: text/html\n< Content-Length: 8474\nContent-Length: 8474\n< Last-Modified: Mon, 20 Feb 2023 17:42:39 GMT\nLast-Modified: Mon, 20 Feb 2023 17:42:39 GMT\n< Connection: keep-alive\nConnection: keep-alive\n< ETag: \"63f3b10f-211a\"\nETag: \"63f3b10f-211a\"\n< Accept-Ranges: bytes\nAccept-Ranges: bytes\n```\n\nLastly, 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:\n```\ncurl -I -v --tlsv1.1 --tls-max 1.1 https://<your.fully.qualified.domain.name>\n```\n\nThe response should be similar to the following:\n```\n* Rebuilt URL to: https://your.fully.qualified.domain.name/\n*   Trying 123.123.123.123...\n* TCP_NODELAY set\n* Connected to your.fully.qualified.domain.name (123.123.123.123) port 443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* successfully set certificate verify locations:\n*   CAfile: /etc/pki/tls/certs/ca-bundle.crt\n  CApath: none\n* TLSv1.3 (OUT), TLS alert, internal error (592):\n* error:141E70BF:SSL routines:tls_construct_client_hello:no protocols available\ncurl: (35) error:141E70BF:SSL routines:tls_construct_client_hello:no protocols available\n```\n\n> [!IMPORTANT]\n> TLS 1.2 and TLS 1.3 support is provided by OpenSSL.\n>\n> 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.\n> \n> If your distribution does not provide these, then please raise this with your distribution or upgrade your distribution to one that does.\n\n> [!NOTE]\n> 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.\n\n> [!NOTE]\n> 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.\n\n\n### Step 7: Secure your 'nginx' configuration to only allow Microsoft 365 to connect\nEnhance 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.\n\n> [!IMPORTANT]\n> 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.\n> It is recommended to automate these updates accordingly and is also beyond the scope of this document on how to do this.\n\n### Step 8: Test your 'onedrive' application using this configuration\n\n*  Run the 'onedrive' application using `--monitor --verbose` and the client should now create a new subscription and register itself:\n```\n.....\nPerforming initial synchronisation to ensure consistent local state ...\nStarted webhook server\nInitializing subscription for updates ...\nWebhook: handled validation request\nCreated new subscription a09ba1cf-3420-4d78-9117-b41373de33ff with expiration: 2024-08-28T08:42:00.637Z\nAttempting to contact Microsoft OneDrive Login Service\nSuccessfully reached Microsoft OneDrive Login Service\nStarting a sync with Microsoft OneDrive\n.....\n```\n\n*  Review the 'nginx' logs to validate that applicable communication is occurring:\n```\n70.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 \"-\" \"-\" \"-\"\n137.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 \"-\" \"-\" \"-\"\n\n```\n\n## Troubleshooting\nIn 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:\n```\nERROR: Microsoft OneDrive API returned an error with the following message:\n  Error Message:    HTTP request returned status code 400 (Bad Request)\n  Error Reason:     Subscription validation request failed. Notification endpoint must respond with 200 OK to validation request.\n  Error Code:       ValidationError\n  Error Timestamp:  2024-08-28T08:22:34\n  API Request ID:   36684746-1458-4150-aeab-9871355a106c\n  Calling Function: logSubscriptionError()\n```\n\nTo correct this issue, use the `setsebool` tool to allow HTTPD processes (which includes 'nginx') to make network connections:\n```\nsudo setsebool -P httpd_can_network_connect 1\n```\nAfter setting the boolean, restart 'nginx' to apply the SELinux configuration change.\n\n## Resulting configuration\n\nWhen these steps are followed, your environment configuration will be similar to the following diagram:\n\n![webhooks](./puml/webhooks.png)\n\n## Additional Configuration Assistance\n\nRefer to [application-config-options.md](application-config-options.md) for further guidance on 'webhook' configuration options."
  },
  {
    "path": "install-sh",
    "content": "#!/bin/sh\n# install - install a program, script, or datafile\n\nscriptversion=2018-03-11.20; # UTC\n\n# This originates from X11R5 (mit/util/scripts/install.sh), which was\n# later released in X11R6 (xc/config/util/install.sh) with the\n# following copyright and license.\n#\n# Copyright (C) 1994 X Consortium\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to\n# deal in the Software without restriction, including without limitation the\n# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n# sell copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE\n# X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN\n# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-\n# TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n#\n# Except as contained in this notice, the name of the X Consortium shall not\n# be used in advertising or otherwise to promote the sale, use or other deal-\n# ings in this Software without prior written authorization from the X Consor-\n# tium.\n#\n#\n# FSF changes to this file are in the public domain.\n#\n# Calling this script install-sh is preferred over install.sh, to prevent\n# 'make' implicit rules from creating a file called install from it\n# when there is no Makefile.\n#\n# This script is compatible with the BSD install script, but was written\n# from scratch.\n\ntab='\t'\nnl='\n'\nIFS=\" $tab$nl\"\n\n# Set DOITPROG to \"echo\" to test this script.\n\ndoit=${DOITPROG-}\ndoit_exec=${doit:-exec}\n\n# Put in absolute file names if you don't have them in your path;\n# or use environment vars.\n\nchgrpprog=${CHGRPPROG-chgrp}\nchmodprog=${CHMODPROG-chmod}\nchownprog=${CHOWNPROG-chown}\ncmpprog=${CMPPROG-cmp}\ncpprog=${CPPROG-cp}\nmkdirprog=${MKDIRPROG-mkdir}\nmvprog=${MVPROG-mv}\nrmprog=${RMPROG-rm}\nstripprog=${STRIPPROG-strip}\n\nposix_mkdir=\n\n# Desired mode of installed file.\nmode=0755\n\nchgrpcmd=\nchmodcmd=$chmodprog\nchowncmd=\nmvcmd=$mvprog\nrmcmd=\"$rmprog -f\"\nstripcmd=\n\nsrc=\ndst=\ndir_arg=\ndst_arg=\n\ncopy_on_change=false\nis_target_a_directory=possibly\n\nusage=\"\\\nUsage: $0 [OPTION]... [-T] SRCFILE DSTFILE\n   or: $0 [OPTION]... SRCFILES... DIRECTORY\n   or: $0 [OPTION]... -t DIRECTORY SRCFILES...\n   or: $0 [OPTION]... -d DIRECTORIES...\n\nIn the 1st form, copy SRCFILE to DSTFILE.\nIn the 2nd and 3rd, copy all SRCFILES to DIRECTORY.\nIn the 4th, create DIRECTORIES.\n\nOptions:\n     --help     display this help and exit.\n     --version  display version info and exit.\n\n  -c            (ignored)\n  -C            install only if different (preserve the last data modification time)\n  -d            create directories instead of installing files.\n  -g GROUP      $chgrpprog installed files to GROUP.\n  -m MODE       $chmodprog installed files to MODE.\n  -o USER       $chownprog installed files to USER.\n  -s            $stripprog installed files.\n  -t DIRECTORY  install into DIRECTORY.\n  -T            report an error if DSTFILE is a directory.\n\nEnvironment variables override the default commands:\n  CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG\n  RMPROG STRIPPROG\n\"\n\nwhile test $# -ne 0; do\n  case $1 in\n    -c) ;;\n\n    -C) copy_on_change=true;;\n\n    -d) dir_arg=true;;\n\n    -g) chgrpcmd=\"$chgrpprog $2\"\n        shift;;\n\n    --help) echo \"$usage\"; exit $?;;\n\n    -m) mode=$2\n        case $mode in\n          *' '* | *\"$tab\"* | *\"$nl\"* | *'*'* | *'?'* | *'['*)\n            echo \"$0: invalid mode: $mode\" >&2\n            exit 1;;\n        esac\n        shift;;\n\n    -o) chowncmd=\"$chownprog $2\"\n        shift;;\n\n    -s) stripcmd=$stripprog;;\n\n    -t)\n        is_target_a_directory=always\n        dst_arg=$2\n        # Protect names problematic for 'test' and other utilities.\n        case $dst_arg in\n          -* | [=\\(\\)!]) dst_arg=./$dst_arg;;\n        esac\n        shift;;\n\n    -T) is_target_a_directory=never;;\n\n    --version) echo \"$0 $scriptversion\"; exit $?;;\n\n    --) shift\n        break;;\n\n    -*) echo \"$0: invalid option: $1\" >&2\n        exit 1;;\n\n    *)  break;;\n  esac\n  shift\ndone\n\n# We allow the use of options -d and -T together, by making -d\n# take the precedence; this is for compatibility with GNU install.\n\nif test -n \"$dir_arg\"; then\n  if test -n \"$dst_arg\"; then\n    echo \"$0: target directory not allowed when installing a directory.\" >&2\n    exit 1\n  fi\nfi\n\nif test $# -ne 0 && test -z \"$dir_arg$dst_arg\"; then\n  # When -d is used, all remaining arguments are directories to create.\n  # When -t is used, the destination is already specified.\n  # Otherwise, the last argument is the destination.  Remove it from $@.\n  for arg\n  do\n    if test -n \"$dst_arg\"; then\n      # $@ is not empty: it contains at least $arg.\n      set fnord \"$@\" \"$dst_arg\"\n      shift # fnord\n    fi\n    shift # arg\n    dst_arg=$arg\n    # Protect names problematic for 'test' and other utilities.\n    case $dst_arg in\n      -* | [=\\(\\)!]) dst_arg=./$dst_arg;;\n    esac\n  done\nfi\n\nif test $# -eq 0; then\n  if test -z \"$dir_arg\"; then\n    echo \"$0: no input file specified.\" >&2\n    exit 1\n  fi\n  # It's OK to call 'install-sh -d' without argument.\n  # This can happen when creating conditional directories.\n  exit 0\nfi\n\nif test -z \"$dir_arg\"; then\n  if test $# -gt 1 || test \"$is_target_a_directory\" = always; then\n    if test ! -d \"$dst_arg\"; then\n      echo \"$0: $dst_arg: Is not a directory.\" >&2\n      exit 1\n    fi\n  fi\nfi\n\nif test -z \"$dir_arg\"; then\n  do_exit='(exit $ret); exit $ret'\n  trap \"ret=129; $do_exit\" 1\n  trap \"ret=130; $do_exit\" 2\n  trap \"ret=141; $do_exit\" 13\n  trap \"ret=143; $do_exit\" 15\n\n  # Set umask so as not to create temps with too-generous modes.\n  # However, 'strip' requires both read and write access to temps.\n  case $mode in\n    # Optimize common cases.\n    *644) cp_umask=133;;\n    *755) cp_umask=22;;\n\n    *[0-7])\n      if test -z \"$stripcmd\"; then\n        u_plus_rw=\n      else\n        u_plus_rw='% 200'\n      fi\n      cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;;\n    *)\n      if test -z \"$stripcmd\"; then\n        u_plus_rw=\n      else\n        u_plus_rw=,u+rw\n      fi\n      cp_umask=$mode$u_plus_rw;;\n  esac\nfi\n\nfor src\ndo\n  # Protect names problematic for 'test' and other utilities.\n  case $src in\n    -* | [=\\(\\)!]) src=./$src;;\n  esac\n\n  if test -n \"$dir_arg\"; then\n    dst=$src\n    dstdir=$dst\n    test -d \"$dstdir\"\n    dstdir_status=$?\n  else\n\n    # Waiting for this to be detected by the \"$cpprog $src $dsttmp\" command\n    # might cause directories to be created, which would be especially bad\n    # if $src (and thus $dsttmp) contains '*'.\n    if test ! -f \"$src\" && test ! -d \"$src\"; then\n      echo \"$0: $src does not exist.\" >&2\n      exit 1\n    fi\n\n    if test -z \"$dst_arg\"; then\n      echo \"$0: no destination specified.\" >&2\n      exit 1\n    fi\n    dst=$dst_arg\n\n    # If destination is a directory, append the input filename.\n    if test -d \"$dst\"; then\n      if test \"$is_target_a_directory\" = never; then\n        echo \"$0: $dst_arg: Is a directory\" >&2\n        exit 1\n      fi\n      dstdir=$dst\n      dstbase=`basename \"$src\"`\n      case $dst in\n\t*/) dst=$dst$dstbase;;\n\t*)  dst=$dst/$dstbase;;\n      esac\n      dstdir_status=0\n    else\n      dstdir=`dirname \"$dst\"`\n      test -d \"$dstdir\"\n      dstdir_status=$?\n    fi\n  fi\n\n  case $dstdir in\n    */) dstdirslash=$dstdir;;\n    *)  dstdirslash=$dstdir/;;\n  esac\n\n  obsolete_mkdir_used=false\n\n  if test $dstdir_status != 0; then\n    case $posix_mkdir in\n      '')\n        # Create intermediate dirs using mode 755 as modified by the umask.\n        # This is like FreeBSD 'install' as of 1997-10-28.\n        umask=`umask`\n        case $stripcmd.$umask in\n          # Optimize common cases.\n          *[2367][2367]) mkdir_umask=$umask;;\n          .*0[02][02] | .[02][02] | .[02]) mkdir_umask=22;;\n\n          *[0-7])\n            mkdir_umask=`expr $umask + 22 \\\n              - $umask % 100 % 40 + $umask % 20 \\\n              - $umask % 10 % 4 + $umask % 2\n            `;;\n          *) mkdir_umask=$umask,go-w;;\n        esac\n\n        # With -d, create the new directory with the user-specified mode.\n        # Otherwise, rely on $mkdir_umask.\n        if test -n \"$dir_arg\"; then\n          mkdir_mode=-m$mode\n        else\n          mkdir_mode=\n        fi\n\n        posix_mkdir=false\n        case $umask in\n          *[123567][0-7][0-7])\n            # POSIX mkdir -p sets u+wx bits regardless of umask, which\n            # is incompatible with FreeBSD 'install' when (umask & 300) != 0.\n            ;;\n          *)\n            # Note that $RANDOM variable is not portable (e.g. dash);  Use it\n            # here however when possible just to lower collision chance.\n            tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$\n\n            trap 'ret=$?; rmdir \"$tmpdir/a/b\" \"$tmpdir/a\" \"$tmpdir\" 2>/dev/null; exit $ret' 0\n\n            # Because \"mkdir -p\" follows existing symlinks and we likely work\n            # directly in world-writeable /tmp, make sure that the '$tmpdir'\n            # directory is successfully created first before we actually test\n            # 'mkdir -p' feature.\n            if (umask $mkdir_umask &&\n                $mkdirprog $mkdir_mode \"$tmpdir\" &&\n                exec $mkdirprog $mkdir_mode -p -- \"$tmpdir/a/b\") >/dev/null 2>&1\n            then\n              if test -z \"$dir_arg\" || {\n                   # Check for POSIX incompatibilities with -m.\n                   # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or\n                   # other-writable bit of parent directory when it shouldn't.\n                   # FreeBSD 6.1 mkdir -m -p sets mode of existing directory.\n                   test_tmpdir=\"$tmpdir/a\"\n                   ls_ld_tmpdir=`ls -ld \"$test_tmpdir\"`\n                   case $ls_ld_tmpdir in\n                     d????-?r-*) different_mode=700;;\n                     d????-?--*) different_mode=755;;\n                     *) false;;\n                   esac &&\n                   $mkdirprog -m$different_mode -p -- \"$test_tmpdir\" && {\n                     ls_ld_tmpdir_1=`ls -ld \"$test_tmpdir\"`\n                     test \"$ls_ld_tmpdir\" = \"$ls_ld_tmpdir_1\"\n                   }\n                 }\n              then posix_mkdir=:\n              fi\n              rmdir \"$tmpdir/a/b\" \"$tmpdir/a\" \"$tmpdir\"\n            else\n              # Remove any dirs left behind by ancient mkdir implementations.\n              rmdir ./$mkdir_mode ./-p ./-- \"$tmpdir\" 2>/dev/null\n            fi\n            trap '' 0;;\n        esac;;\n    esac\n\n    if\n      $posix_mkdir && (\n        umask $mkdir_umask &&\n        $doit_exec $mkdirprog $mkdir_mode -p -- \"$dstdir\"\n      )\n    then :\n    else\n\n      # The umask is ridiculous, or mkdir does not conform to POSIX,\n      # or it failed possibly due to a race condition.  Create the\n      # directory the slow way, step by step, checking for races as we go.\n\n      case $dstdir in\n        /*) prefix='/';;\n        [-=\\(\\)!]*) prefix='./';;\n        *)  prefix='';;\n      esac\n\n      oIFS=$IFS\n      IFS=/\n      set -f\n      set fnord $dstdir\n      shift\n      set +f\n      IFS=$oIFS\n\n      prefixes=\n\n      for d\n      do\n        test X\"$d\" = X && continue\n\n        prefix=$prefix$d\n        if test -d \"$prefix\"; then\n          prefixes=\n        else\n          if $posix_mkdir; then\n            (umask=$mkdir_umask &&\n             $doit_exec $mkdirprog $mkdir_mode -p -- \"$dstdir\") && break\n            # Don't fail if two instances are running concurrently.\n            test -d \"$prefix\" || exit 1\n          else\n            case $prefix in\n              *\\'*) qprefix=`echo \"$prefix\" | sed \"s/'/'\\\\\\\\\\\\\\\\''/g\"`;;\n              *) qprefix=$prefix;;\n            esac\n            prefixes=\"$prefixes '$qprefix'\"\n          fi\n        fi\n        prefix=$prefix/\n      done\n\n      if test -n \"$prefixes\"; then\n        # Don't fail if two instances are running concurrently.\n        (umask $mkdir_umask &&\n         eval \"\\$doit_exec \\$mkdirprog $prefixes\") ||\n          test -d \"$dstdir\" || exit 1\n        obsolete_mkdir_used=true\n      fi\n    fi\n  fi\n\n  if test -n \"$dir_arg\"; then\n    { test -z \"$chowncmd\" || $doit $chowncmd \"$dst\"; } &&\n    { test -z \"$chgrpcmd\" || $doit $chgrpcmd \"$dst\"; } &&\n    { test \"$obsolete_mkdir_used$chowncmd$chgrpcmd\" = false ||\n      test -z \"$chmodcmd\" || $doit $chmodcmd $mode \"$dst\"; } || exit 1\n  else\n\n    # Make a couple of temp file names in the proper directory.\n    dsttmp=${dstdirslash}_inst.$$_\n    rmtmp=${dstdirslash}_rm.$$_\n\n    # Trap to clean up those temp files at exit.\n    trap 'ret=$?; rm -f \"$dsttmp\" \"$rmtmp\" && exit $ret' 0\n\n    # Copy the file name to the temp name.\n    (umask $cp_umask && $doit_exec $cpprog \"$src\" \"$dsttmp\") &&\n\n    # and set any options; do chmod last to preserve setuid bits.\n    #\n    # If any of these fail, we abort the whole thing.  If we want to\n    # ignore errors from any of these, just make sure not to ignore\n    # errors from the above \"$doit $cpprog $src $dsttmp\" command.\n    #\n    { test -z \"$chowncmd\" || $doit $chowncmd \"$dsttmp\"; } &&\n    { test -z \"$chgrpcmd\" || $doit $chgrpcmd \"$dsttmp\"; } &&\n    { test -z \"$stripcmd\" || $doit $stripcmd \"$dsttmp\"; } &&\n    { test -z \"$chmodcmd\" || $doit $chmodcmd $mode \"$dsttmp\"; } &&\n\n    # If -C, don't bother to copy if it wouldn't change the file.\n    if $copy_on_change &&\n       old=`LC_ALL=C ls -dlL \"$dst\"     2>/dev/null` &&\n       new=`LC_ALL=C ls -dlL \"$dsttmp\"  2>/dev/null` &&\n       set -f &&\n       set X $old && old=:$2:$4:$5:$6 &&\n       set X $new && new=:$2:$4:$5:$6 &&\n       set +f &&\n       test \"$old\" = \"$new\" &&\n       $cmpprog \"$dst\" \"$dsttmp\" >/dev/null 2>&1\n    then\n      rm -f \"$dsttmp\"\n    else\n      # Rename the file to the real destination.\n      $doit $mvcmd -f \"$dsttmp\" \"$dst\" 2>/dev/null ||\n\n      # The rename failed, perhaps because mv can't rename something else\n      # to itself, or perhaps because mv is so ancient that it does not\n      # support -f.\n      {\n        # Now remove or move aside any old file at destination location.\n        # We try this two ways since rm can't unlink itself on some\n        # systems and the destination file might be busy for other\n        # reasons.  In this case, the final cleanup might fail but the new\n        # file should still install successfully.\n        {\n          test ! -f \"$dst\" ||\n          $doit $rmcmd -f \"$dst\" 2>/dev/null ||\n          { $doit $mvcmd -f \"$dst\" \"$rmtmp\" 2>/dev/null &&\n            { $doit $rmcmd -f \"$rmtmp\" 2>/dev/null; :; }\n          } ||\n          { echo \"$0: cannot unlink or rename $dst\" >&2\n            (exit 1); exit 1\n          }\n        } &&\n\n        # Now rename the file to the real destination.\n        $doit $mvcmd \"$dsttmp\" \"$dst\"\n      }\n    fi || exit 1\n\n    trap '' 0\n  fi\ndone\n\n# Local variables:\n# eval: (add-hook 'before-save-hook 'time-stamp)\n# time-stamp-start: \"scriptversion=\"\n# time-stamp-format: \"%:y-%02m-%02d.%02H\"\n# time-stamp-time-zone: \"UTC0\"\n# time-stamp-end: \"; # UTC\"\n# End:\n"
  },
  {
    "path": "onedrive.1.in",
    "content": ".TH ONEDRIVE \"1\" \"@PACKAGE_DATE@\" \"@PACKAGE_VERSION@\" \"User Commands\"\n.SH NAME\nonedrive \\- A client for the Microsoft OneDrive Cloud Service\n.SH SYNOPSIS\n.B onedrive\n[\\fI\\,OPTION\\/\\fR] --sync\n.br\n.B onedrive\n[\\fI\\,OPTION\\/\\fR] --monitor\n.br\n.B onedrive\n[\\fI\\,OPTION\\/\\fR] --display-config\n.br\n.B onedrive\n[\\fI\\,OPTION\\/\\fR] --display-sync-status\n.br\n.B onedrive\n[\\fI\\,OPTION\\/\\fR] -h | --help\n.br\n.B onedrive\n--version\n.SH DESCRIPTION\nA fully featured, free, and actively maintained Microsoft OneDrive client that seamlessly supports OneDrive Personal, OneDrive for Business, Microsoft 365 (formerly Office 365), and SharePoint document libraries.\n.PP\nDesigned for maximum flexibility and reliability, this powerful and highly configurable client works across all major Linux distributions and FreeBSD. It can also be deployed in containerised environments using Docker or Podman. Supporting both one-way and two-way synchronisation modes, the client provides secure and efficient file syncing with Microsoft OneDrive services — tailored to suit both desktop and server environments.\n\n.SH FEATURES\n.br\n* Compatible with OneDrive Personal, OneDrive for Business, and Microsoft SharePoint Libraries\n.br\n* Provides full support for shared folders and files across both Personal and Business accounts\n.br\n* Supports single-tenant and multi-tenant Microsoft Entra ID environments\n.br\n* Supports national cloud deployments including Microsoft Cloud for US Government, Microsoft Cloud Germany, and Azure/Office 365 operated by VNET in China\n.br\n* Supports bi-directional synchronisation (default) to keep local and remote data fully aligned\n.br\n* Supports upload-only mode to upload local changes without downloading remote changes\n.br\n* Supports download-only mode to download remote changes without uploading local changes\n.br\n* Supports a dry-run mode for safely testing configuration changes without modifying data\n.br\n* Implements safe conflict handling to minimise data loss by creating local backups when this is determined to be the safest resolution strategy\n.br\n* Provides comprehensive rules-based client-side filtering with inclusions, exclusions, wildcard matching (*), and recursive globbing (**)\n.br\n* Allows selective synchronisation of specific files, directories, or patterns\n.br\n* Caches synchronisation state for efficient processing and improved performance on large or complex sync sets\n.br\n* Supports near real-time processing of cloud-side changes using native WebSocket support\n.br\n* Supports webhook-based online change notifications where WebSockets are unsuitable (manual configuration required)\n.br\n* Monitors local file system changes in real-time using inotify\n.br\n* Implements the FreeDesktop.org Trash specification, enabling recovery of files deleted locally due to remote deletions\n.br\n* Protects against accidental data loss following configuration changes\n.br\n* Supports interruption-tolerant uploads and downloads with automatic transfer resumption\n.br\n* Validates file transfers to ensure data integrity\n.br\n* Enhances synchronisation performance through multi-threaded file transfers\n.br\n* Manages network usage through configurable bandwidth rate limiting\n.br\n* Supports desktop notifications for synchronisation events, warnings, and errors using libnotify\n.br\n* Provides desktop file-manager integration by registering the OneDrive folder as a sidebar location with a distinctive icon\n.br\n* Operates fully in both graphical and headless/server environments, with a graphical environment required only for Intune SSO, desktop notifications, and sidebar integration\n\n\n.SH CONFIGURATION\nBy default, the OneDrive Client for Linux uses a sensible set of built-in defaults to interact with the Microsoft OneDrive service.\n\n.PP\nThe client determines its configuration from three layers, applied in the following order of priority:\n\n.PP\n1. Application default values – internal defaults compiled into the client.\n.br\n2. Configuration file values – user-defined settings loaded from a configuration file (if present).\n.br\n3. Command-line arguments – values specified at runtime override both the configuration file and application defaults.\n.br\n.PP\nThe built-in application defaults are sufficient for most users and provide a reliable operational baseline. Creating a configuration file or using command-line options is optional, and only required when you wish to customise runtime behaviour.\n\n.TP\n.B NOTE:\nThe OneDrive Client does not create a configuration file automatically. If no configuration file is found, the client runs entirely using its internally defined default values. You only need to create a configuration file if you wish to override those defaults.\n\n.PP\nIf you want to adjust the default settings, download a copy of the default configuration template into your local configuration directory.\nValid configuration file locations are:\n.br\n.PP\n\\fB~/.config/onedrive\\fP – for per-user configuration.\n.br\n\\fB/etc/onedrive\\fP – for system-wide configuration.\n\n.TP\n.B Example:\nTo download a copy of the default configuration template, run:\n.PP\n.nf\n\\fB\nmkdir -p ~/.config/onedrive\nwget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/onedrive/config\n\\fP\n.fi\n\n.PP\nFor a full list of configuration options and command-line switches, refer to the online documentation:\n.br\n\\fIhttps://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md\\fP\n\n.SH CLIENT SIDE FILTERING\nClient 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:\n.TP\n.B --skip-dir or 'skip_dir' config file option\nSpecifies directories that should not be synchronised with OneDrive. Useful for omitting large or irrelevant directories from the sync process.\n.TP\n.B --skip-dot-files or 'skip_dotfiles' config file option\nExcludes dotfiles, usually configuration files or scripts, from the sync. Ideal for users who prefer to keep these files local.\n.TP\n.B --skip-file or 'skip_file' config file option\nAllows specifying specific files to exclude from synchronisation. Offers flexibility in selecting essential files for cloud storage.\n.TP\n.B --skip-symlinks or 'skip_symlinks' config file option\nPrevents symlinks, which often point to files outside the OneDrive directory or to irrelevant locations, from being included in the sync.\n.PP\nAdditionally, 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.\n.PP\nThese 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.\n.TP\n.B NOTE:\nAfter changing any Client Side Filtering rule, a full re-synchronisation must be performed using --resync\n\n.SH FIRST RUN\nOnce 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.\n.TP\nPlease 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.\n.TP\nWhen 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.\n.TP\nThis 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.\n\n.SH GUI NOTIFICATIONS\nIf 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.\n\n.SH APPLICATION LOGGING\nWhen 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.\n\n.SH ALL CLI OPTIONS\nThe options below allow you to control the behaviour 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.\n\n.TP\n\\fB\\-\\-sync\\fR, -s\nDo a one-time synchronisation with Microsoft OneDrive.\n\n.TP\n\\fB\\-\\-monitor\\fR, -m\nMonitor filesystem and synchronise regularly with Microsoft OneDrive.\n\n.TP\n\\fB\\-\\-display-config\\fR\nDisplay the currently used configuration for the onedrive client.\n\n.TP\n\\fB\\-\\-display-sync-status\\fR\nQuery OneDrive service and report on pending changes.\n\n.TP\n\\fB\\-\\-auth-files\\fR \\fIARG\\fR\nPerform authentication not via interactive dialogue 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.\nThe 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.\n.br\nAlways specify the full path when using this option, otherwise the application will default to using the default configuration path for these files (~/.config/onedrive/)\n\n.TP\n\\fB\\-\\-auth-response\\fR \\fIARG\\fR\nPerform authentication not via interactive dialogue but via providing the response URL directly.\n\n.TP\n\\fB\\-\\-check-for-nomount\\fR\nCheck for the presence of .nosync in the syncdir root. If found, do not perform sync.\n\n.TP\n\\fB\\-\\-check-for-nosync\\fR\nCheck for the presence of .nosync in each directory. If found, skip directory from sync.\n\n.TP\n\\fB\\-\\-classify-as-big-delete\\fR \\fIARG\\fR\nNumber of children in a path that is locally removed which will be classified as a 'big data delete'.\n\n.TP\n\\fB\\-\\-cleanup-local-files\\fR\nClean up additional local files when using --download-only. This will remove local data.\n\n.TP\n\\fB\\-\\-confdir\\fR \\fIARG\\fR\nSet the directory used to store the configuration files.\n\n.TP\n\\fB\\-\\-create-directory\\fR \\fIARG\\fR\nCreate a directory on OneDrive. No synchronisation will be performed.\n\n.TP\n\\fB\\-\\-create-share-link\\fR \\fIARG\\fR\nCreate a shareable link for an existing file on OneDrive.\n.br\nUse --with-editing-perms to create a read-write share link instead of read-only.\n.br\nUse --share-password <password> to protect the shared link with a password.\n\n.TP\n\\fB\\-\\-debug-https\\fR\nDebug OneDrive HTTPS communication.\n\n.TP\n\\fB\\-\\-destination-directory\\fR \\fIARG\\fR\nDestination directory for renamed or moved items on OneDrive. No synchronisation will be performed.\n\n.TP\n\\fB\\-\\-disable-download-validation\\fR\nDisable download validation when downloading from OneDrive.\n\n.TP\n\\fB\\-\\-disable-notifications\\fR\nDo not use desktop notifications in monitor mode.\n\n.TP\n\\fB\\-\\-disable-upload-validation\\fR\nDisable upload validation when uploading to OneDrive.\n\n.TP\n\\fB\\-\\-display-quota\\fR\nDisplay the quota status of the client. No synchronisation will be performed.\n\n.TP\n\\fB\\-\\-download-file\\fR \\fIARG\\fR\nDownload a single file from Microsoft OneDrive.\n.br\nSpecify the full online path to the file. No synchronisation will be performed.\n\n.TP\n\\fB\\-\\-display-running-config\\fR\nDisplay what options the client has been configured to use on application startup.\n\n.TP\n\\fB\\-\\-download-only\\fR\nReplicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive.\n\n.TP\n\\fB\\-\\-dry-run\\fR\nPerform a trial sync with no changes made.\n\n.TP\n\\fB\\-\\-enable-logging\\fR\nEnable client activity to a separate log file.\n\n.TP\n\\fB\\-\\-file-fragment-size\\fR \\fIARG\\fR\nSpecify the file fragment size for large file uploads (in MB).\n\n.TP\n\\fB\\-\\-force\\fR\nForce the deletion of data when a 'big delete' is detected.\n\n.TP\n\\fB\\-\\-force-http-11\\fR\nForce the use of HTTP 1.1 for all operations.\n\n.TP\n\\fB\\-\\-force-sync\\fR\nForce a synchronisation of a specific folder, only when using --sync --single-directory and ignore all non-default skip_dir and skip_file rules.\n\n.TP\n\\fB\\-\\-get-O365-drive-id\\fR \\fIARG\\fR\nQuery and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library (DEPRECATED).\n\n.TP\n\\fB\\-\\-get-file-link\\fR \\fIARG\\fR\nDisplay the file link of a synced file.\n\n.TP\n\\fB\\-\\-get-sharepoint-drive-id\\fR \\fIARG\\fR\nQuery and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library.\n\n.TP\n\\fB\\-\\-help\\fR, \\fB\\-h\\fR\nDisplay application help.\n\n.TP\n\\fB\\-\\-list-shared-items\\fR\nList OneDrive Business Shared Items.\n\n.TP\n\\fB\\-\\-local-first\\fR\nSynchronise from the local directory source first, before downloading changes from OneDrive.\n\n.TP\n\\fB\\-\\-log-dir\\fR \\fIARG\\fR\nDirectory where logging output is saved to, needs to end with a slash.\n\n.TP\n\\fB\\-\\-logout\\fR\nLog out the current user.\n\n.TP\n\\fB\\-\\-modified-by\\fR \\fIARG\\fR\nDisplay the last modified by details of a given path.\n\n.TP\n\\fB\\-\\-monitor-fullscan-frequency\\fR \\fIARG\\fR\nNumber of sync runs before performing a full local scan of the synced directory.\n\n.TP\n\\fB\\-\\-monitor-interval\\fR \\fIARG\\fR\nNumber of seconds by which each sync operation is undertaken when idle under monitor mode.\n\n.TP\n\\fB\\-\\-monitor-log-frequency\\fR \\fIARG\\fR\nFrequency of logging in monitor mode.\n\n.TP\n\\fB\\-\\-no-remote-delete\\fR\nDo not delete local file 'deletes' from OneDrive when using --upload-only.\n\n.TP\n\\fB\\-\\-print-access-token\\fR\nPrint the access token, useful for debugging.\n\n.TP\n\\fB\\-\\-reauth\\fR\nReauthenticate the client with OneDrive.\n\n.TP\n\\fB\\-\\-remove-directory\\fR \\fIARG\\fR\nRemove a directory on OneDrive. No synchronisation will be performed.\n\n.TP\n\\fB\\-\\-remove-source-files\\fR\nRemove source file after successful transfer to OneDrive when using --upload-only.\n\n.TP\n\\fB\\-\\-remove-source-folders\\fR\nRemove the local directory structure post successful file transfer to Microsoft OneDrive when using --upload-only --remove-source-files.\n\n.TP\n\\fB\\-\\-resync\\fR\nForget the last saved state, perform a full sync.\n\n.TP\n\\fB\\-\\-resync-auth\\fR\nApprove the use of performing a --resync action.\n\n.TP\n\\fB\\-\\-share-password\\fR \\fIARG\\fR\nRequire a password to access the shared link when used with --create-share-link <file>.\nOnly supported for OneDrive Business and SharePoint environments that permit password-protected sharing.\n\n.TP\n\\fB\\-\\-single-directory\\fR \\fIARG\\fR\nSpecify a single local directory within the OneDrive root to sync.\n\n.TP\n\\fB\\-\\-skip-dir\\fR \\fIARG\\fR\nSkip any directories that match this pattern from syncing.\n\n.TP\n\\fB\\-\\-skip-dir-strict-match\\fR\nWhen matching skip_dir directories, only match explicit matches.\n\n.TP\n\\fB\\-\\-skip-dot-files\\fR\nSkip dot files and folders from syncing.\n\n.TP\n\\fB\\-\\-skip-file\\fR \\fIARG\\fR\nSkip any files that match this pattern from syncing.\n\n.TP\n\\fB\\-\\-skip-size\\fR \\fIARG\\fR\nSkip new files larger than this size (in MB).\n\n.TP\n\\fB\\-\\-skip-symlinks\\fR\nSkip syncing of symlinks.\n\n.TP\n\\fB\\-\\-source-directory\\fR \\fIARG\\fR\nSource directory to rename or move on OneDrive. No synchronisation will be performed.\n\n.TP\n\\fB\\-\\-space-reservation\\fR \\fIARG\\fR\nThe amount of disk space to reserve (in MB) to avoid 100% disk space utilisation.\n\n.TP\n\\fB\\-\\-sync-root-files\\fR\nSync all files in sync_dir root when using sync_list.\n\n.TP\n\\fB\\-\\-sync-shared-files\\fR\nSync OneDrive Business Shared Files to the local filesystem.\n\n.TP\n\\fB\\-\\-syncdir\\fR \\fIARG\\fR\nSpecify the local directory used for synchronisation to OneDrive.\n\n.TP\n\\fB\\-\\-synchronize\\fR\nPerform a synchronisation with Microsoft OneDrive (DEPRECATED).\n\n.TP\n\\fB\\-\\-threads\\fR \\fIARG\\fR\nSpecify a value for the number of worker threads used for parallel upload and download operations.\n\n.TP\n\\fB\\-\\-upload-only\\fR\nReplicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive.\n\n.TP\n\\fB\\-\\-verbose\\fR, \\fB\\-v+\\fR\nPrint more details, useful for debugging (repeat for extra debugging).\n\n.TP\n\\fB\\-\\-version\\fR\nPrint the version and exit.\n\n.TP\n\\fB\\-\\-with-editing-perms\\fR\nCreate a read-write shareable link for an existing file on OneDrive when used with --create-share-link <file>.\n\n.SH DOCUMENTATION\nAll documentation is available on GitHub: https://github.com/abraunegg/onedrive/tree/master/docs/\n\n\n.SH SEE ALSO\n.BR curl(1),\n"
  },
  {
    "path": "readme.md",
    "content": "# OneDrive Client for Linux \n[![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases)\n[![Release Date](https://img.shields.io/github/release-date/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases)\n[![Test Build](https://github.com/abraunegg/onedrive/actions/workflows/testbuild.yaml/badge.svg)](https://github.com/abraunegg/onedrive/actions/workflows/testbuild.yaml)\n[![Build Docker Images](https://github.com/abraunegg/onedrive/actions/workflows/docker.yaml/badge.svg)](https://github.com/abraunegg/onedrive/actions/workflows/docker.yaml)\n[![Docker Pulls](https://img.shields.io/docker/pulls/driveone/onedrive)](https://hub.docker.com/r/driveone/onedrive)\n\nA fully featured, free, and actively maintained Microsoft OneDrive client that seamlessly supports OneDrive Personal, OneDrive for Business, Microsoft 365 (formerly Office 365), and SharePoint document libraries.\n\nDesigned for maximum flexibility and reliability, this powerful and highly configurable client works across all major Linux distributions and FreeBSD. It can also be deployed in containerised environments using Docker or Podman. Supporting both one-way and two-way synchronisation modes, the client provides secure and efficient file syncing with Microsoft OneDrive services — tailored to suit both desktop and server environments.\n\n\n## Project Background\nThis project originated as a fork of the skilion client in early 2018, after a number of proposed improvements and bug fixes — including [Pull Requests #82 and #314](https://github.com/skilion/onedrive/pulls?q=author%3Aabraunegg) — were not merged and development activity of the skilion client had largely stalled. While it’s unclear whether the original developer was unavailable or had stepped away from the project - bug reports and feature requests remained unanswered for extended periods. In 2020, the original developer (skilion) confirmed they had no intention of maintaining or supporting their work ([reference](https://github.com/skilion/onedrive/issues/518#issuecomment-717604726)).\n\nThe original [skilion repository](https://github.com/skilion/onedrive) was formally archived and made read-only on GitHub in December 2024. While still publicly accessible as a historical reference, an archived repository is no longer maintained, cannot accept contributions, and reflects a frozen snapshot of the codebase. The last code change to the skilion client was merged in November 2021; however, active development had slowed significantly well before then. As such, the skilion client should no longer be considered current or supported — particularly given the major API changes and evolving Microsoft OneDrive platform requirements since that time.\n\nUnder the terms of the GNU General Public License (GPL), forking and continuing development of open source software is fully permitted — provided that derivative works retain the same license. This client complies with the original GPLv3 licensing, ensuring the same freedoms granted by the original project remain intact.\n\nSince forking in early 2018, this client has evolved into a clean re-imagining of the original codebase, resolving long-standing bugs and adding extensive new functionality to better support both personal and enterprise use cases to interact with Microsoft OneDrive from Linux and FreeBSD platforms.\n\n\n## Features\n\n### Broad Microsoft OneDrive Compatibility\n\n* Works with OneDrive Personal, OneDrive for Business, and Microsoft SharePoint Libraries.\n* Full support for shared folders and files across both Personal and Business accounts.\n* Supports single-tenant and multi-tenant Microsoft Entra ID environments.\n* Compatible with national cloud deployments:\n  * Microsoft Cloud for US Government\n  * Microsoft Cloud Germany\n  * Azure/Office 365 operated by VNET in China\n\n### Flexible Synchronisation Modes\n\n* Bi-directional sync (default) - keeps local and remote data fully aligned.\n* Upload-only mode - only uploads local changes; does not download remote changes.\n* Download-only mode - only downloads remote changes; does not upload local changes.\n* Dry-run mode - test configuration changes safely without modifying files.\n* Safe conflict handling minimises data loss by creating local backups whenever this is determined to be the safest conflict-resolution strategy.\n\n### Client-Side Filtering & Granular Sync Control\n\n* Comprehensive rules-based client-side filtering (inclusions, exclusions, wildcard `*`, globbing `**`).\n* Filter specific files, folders, or patterns to tailor precisely what is synced with Microsoft OneDrive.\n* Efficient cached sync state for fast decision-making during large or complex sync sets.\n\n### Real-Time Monitoring & Online Change Detection\n\n* Near real-time processing of cloud-side changes using native WebSocket support.\n* Webhook support for environments where WebSockets are unsuitable (manual setup).\n* Real-time local change monitoring via inotify.\n\n### Data Safety, Recovery & Integrity Protection\n\n* Implements the FreeDesktop.org Trash specification, enabling recovery of items deleted locally due to online deletion.\n* Strong safeguards to prevent accidental remote deletion or overwrite after configuration changes.\n* Interruption-tolerant uploads and downloads, automatically resuming transfers.\n* Integrity validation for every file transferred.\n\n### Modern Authentication Support\n\n* Standard OAuth2 Native Client Authorisation Flow (default), supporting browser-based login, multi-factor authentication (MFA), and modern Microsoft account security requirements.\n* OAuth2 Device Authorisation Flow for Microsoft Entra ID accounts, ideal for headless systems, servers, and terminal-only environments.\n* Intune Single Sign-On (SSO) using the Microsoft Identity Device Broker (IDB) via D-Bus, enabling seamless enterprise authentication without manual credential entry.\n\n### Performance, Efficiency & Resource Management\n\n* Multi-threaded file transfers for significantly improved sync speeds.\n* Bandwidth rate limiting to control network consumption.\n* Highly efficient processing with state caching, reducing API traffic and improving performance.\n\n### Desktop Integration & User Experience\n\n* libnotify desktop notifications for sync events, warnings, and errors.\n* Registers the OneDrive folder as a sidebar location in supported file managers, complete with a distinctive icon.\n* Works seamlessly in GUI and headless/server environments. A GUI is only required for Intune SSO, notifications, and sidebar integration; all other features function without graphical support.\n\n\n## What's missing\n*   Ability to encrypt/decrypt files on-the-fly when uploading/downloading files from OneDrive\n*   Support for Windows 'On-Demand' functionality so file is only downloaded when accessed locally\n\n## External Enhancements\n*   A GUI for configuration management: [OneDrive Client for Linux GUI](https://github.com/bpozdena/OneDriveGUI)\n*   Colorful log output terminal modification: [OneDrive Client for Linux Colorful log Output](https://github.com/zzzdeb/dotfiles/blob/master/scripts/tools/onedrive_log)\n*   System Tray Icon: [OneDrive Client for Linux System Tray Icon](https://github.com/DanielBorgesOliveira/onedrive_tray)\n\n## Frequently Asked Questions\nRefer to [Frequently Asked Questions](https://github.com/abraunegg/onedrive/wiki/Frequently-Asked-Questions)\n\n## Have a question\nIf you have a question or need something clarified, please raise a new discussion post [here](https://github.com/abraunegg/onedrive/discussions)\n\n## Supported Application Version\nSupport is only provided for the current application release version or newer 'master' branch versions.\n\nThe current release version is: [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases)\n\nTo check your version, run: `onedrive --version`. Ensure you are using the current release or compile the latest version from the master branch if needed.\n\nIf you are using an older version, you must upgrade to the current release or newer to receive support.\n\n## Documentation and Configuration Assistance\nOneDrive Client for Linux includes a rich set of documentation covering installation, configuration options, advanced usage, and integrations. These resources are designed to help new users get started quickly and to give experienced users full control over advanced behaviour. If you are changing configuration, running in production, or using Business/SharePoint features, you should be reading these documents. All documentation is maintained in the [`docs/`](https://github.com/abraunegg/onedrive/tree/master/docs) directory of this repository.\n\n### Getting Started\n\n#### Installation\nLearn how to install the client on various systems — from distribution packages to building from source. Please read the [Install Guide](https://github.com/abraunegg/onedrive/blob/master/docs/install.md)\n\n#### Basic Usage & Configuration\nCovers initial authentication, default settings, basic operational instructions, frequently asked 'how to' questions, and how to tailor the application configuration. Please read the [Usage Guide](https://github.com/abraunegg/onedrive/blob/master/docs/usage.md)\n\n### Advanced Configuration\n\n#### Application Configuration Options\nFull reference for every config option (with descriptions, defaults, and examples) to customise sync behaviour precisely. Please read the [Application Configuration Options Guide](https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md)\n\n#### Advanced Usage\nTips for creating multiple config profiles, custom sync rules, daemon setups, selective sync, dual-booting with Microsoft Windows and more. Please read the [Advanced Usage Guide](https://github.com/abraunegg/onedrive/blob/master/docs/advanced-usage.md)\n\n### Special Use Cases\n\n#### Business Shared Items\nConfiguring sync for OneDrive Business shared items (files and folders). Please read the [Business Shared Items Guide](https://github.com/abraunegg/onedrive/blob/master/docs/business-shared-items.md)\n\n#### SharePoint & Office 365 Libraries\nInstructions for syncing SharePoint document libraries (Business or Education tenants). Please read the [SharePoint Library Guide](https://github.com/abraunegg/onedrive/blob/master/docs/sharepoint-libraries.md)\n\n#### National Cloud support\nInstructions for environments like Microsoft Cloud Germany or US Government cloud endpoints. Please read the [National Cloud Deployment Guide](https://github.com/abraunegg/onedrive/blob/master/docs/national-cloud-deployments.md)\n\n### Container Support\n\n#### Docker\nHow to run the OneDrive client in a Docker container. Please read the [Docker Guide](https://github.com/abraunegg/onedrive/blob/master/docs/docker.md)\n\n#### Podman\nHow to run the OneDrive client with Podman. Please read the [Podman Guide](https://github.com/abraunegg/onedrive/blob/master/docs/podman.md)\n\n\n## Basic Troubleshooting Steps\n\nIf you encounter any issues running the application, please follow these steps **before** raising a bug report:\n\n1. **Check the application version**  \n   Run `onedrive --version` to confirm which version you are using.  \n   - Ensure you are running the latest [release](https://github.com/abraunegg/onedrive/releases).  \n   - If you are already on the latest release but still experiencing issues, manually build the client from the `master` branch to test against the very latest code. This includes fixes for bugs discovered since the last tagged release.\n   - If you are using Docker or Podman, ensure you are using the 'edge' Docker Tag. Do not use the 'latest' Docker Tag.\n\n2. **Run in verbose mode**  \n   Use the `--verbose` option to provide greater clarity and detailed logging about the issue you are facing.\n   \n   If you are using Docker or Podman, use the ONEDRIVE_VERBOSE environment variable to increase logging verbosity.\n\n3. **Test with IPv4 only**  \n   Configure the application to use **IPv4 network connectivity only**, then retest. See the `'ip_protocol_version'` option [documentation](https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md#ip_protocol_version) for assistance.\n\n4. **Test with HTTP/1.1 and IPv4**  \n   Configure the application to use **HTTP/1.1 over IPv4 only**, then retest. See the `'force_http_11'` option [documentation](https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md#force_http_11) for assistance.\n\n5. **Verify cURL and libcurl versions**  \n   If the above steps do not resolve your issue, upgrade both `curl` and `libcurl` to the latest versions provided by the curl developers.  \n   - See [Compatibility with curl](https://github.com/abraunegg/onedrive/blob/master/docs/usage.md#compatibility-with-curl) for details on curl bugs that impact this client.  \n   - Refer to the official [cURL Releases](https://curl.se/docs/releases.html) page for version information.\n\n6. **Open a new issue**  \n   If the problem persists after completing the steps above, proceed to **Reporting an Issue or Bug** below and open a new issue with the requested details and logs.\n\n\n\n## Reporting an Issue or Bug\n\n> [!IMPORTANT]\n> Please ensure the problem is a software bug. For installation issues, distribution package/version questions, or dependency problems, start a [Discussion](https://github.com/abraunegg/onedrive/discussions) instead of filing a bug report.\n\nIf you encounter a bug, you can report it on GitHub. Before opening a new issue report:\n\n1. **Complete the Basic Troubleshooting Steps**  \n   Confirm you’ve run through all steps in the section above.\n\n2. **Search existing issues**  \n   Check both [Open](https://github.com/abraunegg/onedrive/issues) and [Closed](https://github.com/abraunegg/onedrive/issues?q=is%3Aissue%20state%3Aclosed) issues for a similar problem to avoid duplicates.\n\n3. **Use the issue template**  \n   Open a new bug report using the [issue template](https://github.com/abraunegg/onedrive/issues/new?template=bug_report.md) and fill in **all fields**. Complete detail helps us reproduce your environment and replicate the issue.\n\n4. **Generate a debug log**  \n   Follow this [process](https://github.com/abraunegg/onedrive/wiki/Generate-debug-log-for-support) to create a debug log.\n\n   - If you are concerned about personal or business sensitive data in the debug log, you may:\n     - Create a new OneDrive account, configure the client to use it, use **dummy** data to simulate your environment, and reproduce the issue; or\n     - Provide an NDA or confidentiality agreement for signature prior to sharing sensitive logs.\n\n5. **Share the debug log securely**\n   - **Do not post debug logs publicly.** Debug logs can include sensitive details (file paths, filenames, API endpoints, environment info, etc.).\n   - **Send the log via email** to **support@mynas.com.au** using a trusted email account.\n   - **Archive and password-protect** the log before sending (e.g. `.zip` with AES or `.7z`):\n     - Example (zip with password): `zip -e onedrive-debug.zip onedrive-debug.log`\n     - Example (7z with password): `7z a -p onedrive-debug.7z onedrive-debug.log`\n   - **Send the password out-of-band (OOB)** — not in the same email as the archive. Email **support@mynas.com.au** to arrange an OOB method (e.g. separate email thread, phone/SMS, or agreed channel).\n   - **If you require an NDA**, attach your NDA or confidentiality agreement to your email. It will be reviewed and signed prior to exchanging sensitive data.\n\n\n### What to include in your bug report\nWhen raising a new bug report, please include **all details requested in the issue template**, such as:\n\n- A clear description of the problem and how to reproduce it  \n- Your operating system and installation method  \n- OneDrive account type and client version  \n- Application configuration and cURL version  \n- Sync directory location, system mount points, and partition types  \n- A full debug log, shared securely as described above  \n\nProviding complete information makes it much easier to understand, reproduce, and resolve your issue quickly.  \n\n> [!NOTE]  \n> Submitting a bug report starts a collaboration. To help us help you, please:  \n> - Stay available to answer questions or provide clarifications if needed  \n> - Test and confirm fixes in your own environment when a pull request (PR) is created for your issue  \n\n> [!TIP]  \n> Reports with missing details are much harder to investigate. Sharing as much as you can up front gives the best chance of a fast and accurate fix.\n\n## Known issues\nLists common limitations, known problems, diagnostics, and workarounds. Please read the [Known Issues Advice](https://github.com/abraunegg/onedrive/blob/master/docs/known-issues.md)\n\n"
  },
  {
    "path": "src/arsd/README.md",
    "content": "The files in this directory have been obtained form the following places:\n\ncgi.d\n\thttps://github.com/adamdruppe/arsd/blob/a870179988b8881b04126856105f0fad2cc0018d/cgi.d\n\tLicense: Boost Software License - Version 1.0\n\n    Copyright 2008-2021, Adam D. Ruppe\n\tsee https://github.com/adamdruppe/arsd/blob/a870179988b8881b04126856105f0fad2cc0018d/LICENSE\n"
  },
  {
    "path": "src/arsd/cgi.d",
    "content": "// FIXME: if an exception is thrown, we shouldn't necessarily cache...\n// FIXME: there's some annoying duplication of code in the various versioned mains\n\n// add the Range header in there too. should return 206\n\n// FIXME: cgi per-request arena allocator\n\n// i need to add a bunch of type templates for validations... mayne @NotNull or NotNull!\n\n// FIXME: I might make a cgi proxy class which can change things; the underlying one is still immutable\n// but the later one can edit and simplify the api. You'd have to use the subclass tho!\n\n/*\nvoid foo(int f, @(\"test\") string s) {}\n\nvoid main() {\n\tstatic if(is(typeof(foo) Params == __parameters))\n\t\t//pragma(msg, __traits(getAttributes, Params[0]));\n\t\tpragma(msg, __traits(getAttributes, Params[1..2]));\n\telse\n\t\tpragma(msg, \"fail\");\n}\n*/\n\n// Note: spawn-fcgi can help with fastcgi on nginx\n\n// FIXME: to do: add openssl optionally\n// make sure embedded_httpd doesn't send two answers if one writes() then dies\n\n// future direction: websocket as a separate process that you can sendfile to for an async passoff of those long-lived connections\n\n/*\n\tSession manager process: it spawns a new process, passing a\n\tcommand line argument, to just be a little key/value store\n\tof some serializable struct. On Windows, it CreateProcess.\n\tOn Linux, it can just fork or maybe fork/exec. The session\n\tkey is in a cookie.\n\n\tServer-side event process: spawns an async manager. You can\n\tpush stuff out to channel ids and the clients listen to it.\n\n\twebsocket process: spawns an async handler. They can talk to\n\teach other or get info from a cgi request.\n\n\tTempting to put web.d 2.0 in here. It would:\n\t\t* map urls and form generation to functions\n\t\t* have data presentation magic\n\t\t* do the skeleton stuff like 1.0\n\t\t* auto-cache generated stuff in files (at least if pure?)\n\t\t* introspect functions in json for consumers\n\n\n\thttps://linux.die.net/man/3/posix_spawn\n*/\n\n/++\n\tProvides 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.\n\n\t---\n\timport arsd.cgi;\n\n\t// Instead of writing your own main(), you should write a function\n\t// that takes a Cgi param, and use mixin GenericMain\n\t// for maximum compatibility with different web servers.\n\tvoid hello(Cgi cgi) {\n\t\tcgi.setResponseContentType(\"text/plain\");\n\n\t\tif(\"name\" in cgi.get)\n\t\t\tcgi.write(\"Hello, \" ~ cgi.get[\"name\"]);\n\t\telse\n\t\t\tcgi.write(\"Hello, world!\");\n\t}\n\n\tmixin GenericMain!hello;\n\t---\n\n\tOr:\n\t---\n\timport arsd.cgi;\n\n\tclass MyApi : WebObject {\n\t\t@UrlName(\"\")\n\t\tstring hello(string name = null) {\n\t\t\tif(name is null)\n\t\t\t\treturn \"Hello, world!\";\n\t\t\telse\n\t\t\t\treturn \"Hello, \" ~ name;\n\t\t}\n\t}\n\tmixin DispatcherMain!(\n\t\t\"/\".serveApi!MyApi\n\t);\n\t---\n\n\t$(NOTE\n\t\tPlease note that using the higher-level api will add a dependency on arsd.dom and arsd.jsvar to your application.\n\t\tIf 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`\n\t\tand `dub add arsd-official:dom` yourself.\n\t)\n\n\tTest on console (works in any interface mode):\n\t$(CONSOLE\n\t\t$ ./cgi_hello GET / name=whatever\n\t)\n\n\tIf using http version (default on `dub` builds, or on custom builds when passing `-version=embedded_httpd` to dmd):\n\t$(CONSOLE\n\t\t$ ./cgi_hello --port 8080\n\t\t# now you can go to http://localhost:8080/?name=whatever\n\t)\n\n\tPlease 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.\n\n\n\tBuild_Configurations:\n\n\tcgi.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`.\n\n\tIf you are using `dub`, use:\n\n\t```sdlang\n\tsubConfiguration \"arsd-official:cgi\" \"VALUE_HERE\"\n\t```\n\n\tor to dub.json:\n\n\t```json\n        \t\"subConfigurations\": {\"arsd-official:cgi\": \"VALUE_HERE\"}\n\t```\n\n\tto change versions. The possible options for `VALUE_HERE` are:\n\n\t$(LIST\n\t\t* `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.\n\t\t* `cgi` for traditional cgi binaries. These are run by an outside web server as-needed to handle requests.\n\t\t* `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.\n\t\t* `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.\n\t\t* `stdio_http` for speaking raw http over stdin and stdout. This is made for systemd services. See [RequestServer.serveSingleHttpConnectionOnStdio] for more information.\n\t)\n\n\tWith dmd, use:\n\n\t$(TABLE_ROWS\n\n\t\t* + Interfaces\n\t\t  + (mutually exclusive)\n\n\t\t* - `-version=plain_cgi`\n\t\t\t- The default building the module alone without dub - a traditional, plain CGI executable will be generated.\n\t\t* - `-version=embedded_httpd`\n\t\t\t- A HTTP server will be embedded in the generated executable. This is default when building with dub.\n\t\t* - `-version=fastcgi`\n\t\t\t- A FastCGI executable will be generated.\n\t\t* - `-version=scgi`\n\t\t\t- A SCGI (SimpleCGI) executable will be generated.\n\t\t* - `-version=embedded_httpd_hybrid`\n\t\t\t- 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.\n\t\t* - `-version=embedded_httpd_threads`\n\t\t\t- 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)\n\t\t* - `-version=embedded_httpd_processes`\n\t\t\t- The embedded HTTP server will use a prefork style process pool. (use instead of plain `embedded_httpd` if you want this specific implementation)\n\t\t* - `-version=embedded_httpd_processes_accept_after_fork`\n\t\t\t- 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.\n\t\t* - `-version=stdio_http`\n\t\t\t- The embedded HTTP server will be spoken over stdin and stdout.\n\n\t\t* + Tweaks\n\t\t  + (can be used together with others)\n\n\t\t* - `-version=cgi_with_websocket`\n\t\t\t- The CGI class has websocket server support. (This is on by default now.)\n\n\t\t* - `-version=with_openssl`\n\t\t\t- not currently used\n\t\t* - `-version=cgi_embedded_sessions`\n\t\t\t- The session server will be embedded in the cgi.d server process\n\t\t* - `-version=cgi_session_server_process`\n\t\t\t- The session will be provided in a separate process, provided by cgi.d.\n\t)\n\n\tFor example,\n\n\tFor CGI, `dmd yourfile.d cgi.d` then put the executable in your cgi-bin directory.\n\n\tFor 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).\n\n\tFor SCGI: `dmd yourfile.d cgi.d -version=scgi` and run the executable, providing a port number on the command line.\n\n\tFor 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.\n\n\tSimulating_requests:\n\n\tIf 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:\n\n\t$(CONSOLE\n\t./yourprogram GET / name=adr\n\t)\n\n\tAnd it will print the result to stdout instead of running a server, regardless of build more..\n\n\tCGI_Setup_tips:\n\n\tOn 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\"`.\n\n\tOverview_Of_Basic_Concepts:\n\n\tcgi.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:\n\n\t\tInput: [Cgi.get], [Cgi.post], [Cgi.request], [Cgi.files], [Cgi.cookies], [Cgi.pathInfo], [Cgi.requestMethod],\n\t\t       and HTTP headers ([Cgi.headers], [Cgi.userAgent], [Cgi.referrer], [Cgi.accept], [Cgi.authorization], [Cgi.lastEventId])\n\n\t\tOutput: [Cgi.write], [Cgi.header], [Cgi.setResponseStatus], [Cgi.setResponseContentType], [Cgi.gzipResponse]\n\n\t\tCookies: [Cgi.setCookie], [Cgi.clearCookie], [Cgi.cookie], [Cgi.cookies]\n\n\t\tCaching: [Cgi.setResponseExpires], [Cgi.updateResponseExpires], [Cgi.setCache]\n\n\t\tRedirections: [Cgi.setResponseLocation]\n\n\t\tOther Information: [Cgi.remoteAddress], [Cgi.https], [Cgi.port], [Cgi.scriptName], [Cgi.requestUri], [Cgi.getCurrentCompleteUri], [Cgi.onRequestBodyDataReceived]\n\n\t\tWebsockets: [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.\n\n\t\tOverriding behavior for special cases streaming input data: see the virtual functions [Cgi.handleIncomingDataChunk], [Cgi.prepareForIncomingDataChunks], [Cgi.cleanUpPostDataState]\n\n\tA basic program using the lower-level api might look like:\n\n\t\t---\n\t\timport arsd.cgi;\n\n\t\t// you write a request handler which always takes a Cgi object\n\t\tvoid handler(Cgi cgi) {\n\t\t\t/+\n\t\t\t\twhen the user goes to your site, suppose you are being hosted at http://example.com/yourapp\n\n\t\t\t\tIf the user goes to http://example.com/yourapp/test?name=value\n\t\t\t\tthen the url will be parsed out into the following pieces:\n\n\t\t\t\t\tcgi.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.)\n\n\t\t\t\t\tcgi.scriptName == \"yourapp\". With an embedded http server, this will be blank.\n\n\t\t\t\t\tcgi.host == \"example.com\"\n\n\t\t\t\t\tcgi.https == false\n\n\t\t\t\t\tcgi.queryString == \"name=value\" (there's also cgi.search, which will be \"?name=value\", including the ?)\n\n\t\t\t\t\tThe query string is further parsed into the `get` and `getArray` members, so:\n\n\t\t\t\t\tcgi.get == [\"name\": \"value\"], meaning you can do `cgi.get[\"name\"] == \"value\"`\n\n\t\t\t\t\tAnd\n\n\t\t\t\t\tcgi.getArray == [\"name\": [\"value\"]].\n\n\t\t\t\t\tWhy is there both `get` and `getArray`? The standard allows names to be repeated. This can be very useful,\n\t\t\t\t\tit is how http forms naturally pass multiple items like a set of checkboxes. So `getArray` is the complete data\n\t\t\t\t\tif you need it. But since so often you only care about one value, the `get` member provides more convenient access.\n\n\t\t\t\tWe 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.\n\t\t\t+/\n\t\t\tswitch(cgi.pathInfo) {\n\t\t\t\t// the home page will be a small html form that can set a cookie.\n\t\t\t\tcase \"/\":\n\t\t\t\t\tcgi.write(`<!DOCTYPE html>\n\t\t\t\t\t<html>\n\t\t\t\t\t<body>\n\t\t\t\t\t\t<form method=\"POST\" action=\"set-cookie\">\n\t\t\t\t\t\t\t<label>Your name: <input type=\"text\" name=\"name\" /></label>\n\t\t\t\t\t\t\t<input type=\"submit\" value=\"Submit\" />\n\t\t\t\t\t\t</form>\n\t\t\t\t\t</body>\n\t\t\t\t\t</html>\n\t\t\t\t\t`, true); // the , true tells it that this is the one, complete response i want to send, allowing some optimizations.\n\t\t\t\tbreak;\n\t\t\t\t// POSTing to this will set a cookie with our submitted name\n\t\t\t\tcase \"/set-cookie\":\n\t\t\t\t\t// HTTP has a number of request methods (also called \"verbs\") to tell\n\t\t\t\t\t// what you should do with the given resource.\n\t\t\t\t\t// The most common are GET and POST, the ones used in html forms.\n\t\t\t\t\t// You can check which one was used with the `cgi.requestMethod` property.\n\t\t\t\t\tif(cgi.requestMethod == Cgi.RequestMethod.POST) {\n\n\t\t\t\t\t\t// headers like redirections need to be set before we call `write`\n\t\t\t\t\t\tcgi.setResponseLocation(\"read-cookie\");\n\n\t\t\t\t\t\t// just like how url params go into cgi.get/getArray, form data submitted in a POST\n\t\t\t\t\t\t// body go to cgi.post/postArray. Please note that a POST request can also have get\n\t\t\t\t\t\t// params in addition to post params.\n\t\t\t\t\t\t//\n\t\t\t\t\t\t// There's also a convenience function `cgi.request(\"name\")` which checks post first,\n\t\t\t\t\t\t// then get if it isn't found there, and then returns a default value if it is in neither.\n\t\t\t\t\t\tif(\"name\" in cgi.post) {\n\t\t\t\t\t\t\t// we can set cookies with a method too\n\t\t\t\t\t\t\t// again, cookies need to be set before calling `cgi.write`, since they\n\t\t\t\t\t\t\t// are a kind of header.\n\t\t\t\t\t\t\tcgi.setCookie(\"name\" , cgi.post[\"name\"]);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// the user will probably never see this, since the response location\n\t\t\t\t\t\t// is an automatic redirect, but it is still best to say something anyway\n\t\t\t\t\t\tcgi.write(\"Redirecting you to see the cookie...\", true);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// you can write out response codes and headers\n\t\t\t\t\t\t// as well as response bodies\n\t\t\t\t\t\t//\n\t\t\t\t\t\t// But always check the cgi docs before using the generic\n\t\t\t\t\t\t// `header` method - if there is a specific method for your\n\t\t\t\t\t\t// header, use it before resorting to the generic one to avoid\n\t\t\t\t\t\t// a header value from being sent twice.\n\t\t\t\t\t\tcgi.setResponseLocation(\"405 Method Not Allowed\");\n\t\t\t\t\t\t// there is no special accept member, so you can use the generic header function\n\t\t\t\t\t\tcgi.header(\"Accept: POST\");\n\t\t\t\t\t\t// but content type does have a method, so prefer to use it:\n\t\t\t\t\t\tcgi.setResponseContentType(\"text/plain\");\n\n\t\t\t\t\t\t// all the headers are buffered, and will be sent upon the first body\n\t\t\t\t\t\t// write. you can actually modify some of them before sending if need be.\n\t\t\t\t\t\tcgi.write(\"You must use the POST http verb on this resource.\", true);\n\t\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t\t// and GETting this will read the cookie back out\n\t\t\t\tcase \"/read-cookie\":\n\t\t\t\t\t// I did NOT pass `,true` here because this is writing a partial response.\n\t\t\t\t\t// It is possible to stream data to the user in chunks by writing partial\n\t\t\t\t\t// responses the calling `cgi.flush();` to send the partial response immediately.\n\t\t\t\t\t// normally, you'd only send partial chunks if you have to - it is better to build\n\t\t\t\t\t// a response as a whole and send it as a whole whenever possible - but here I want\n\t\t\t\t\t// to demo that you can.\n\t\t\t\t\tcgi.write(\"Hello, \");\n\t\t\t\t\tif(\"name\" in cgi.cookies) {\n\t\t\t\t\t\timport arsd.dom; // dom.d provides a lot of helpers for html\n\t\t\t\t\t\t// since the cookie is set, we need to write it out properly to\n\t\t\t\t\t\t// avoid cross-site scripting attacks.\n\t\t\t\t\t\t//\n\t\t\t\t\t\t// Getting this stuff right automatically is a benefit of using the higher\n\t\t\t\t\t\t// level apis, but this demo is to show the fundamental building blocks, so\n\t\t\t\t\t\t// we're responsible to take care of it.\n\t\t\t\t\t\tcgi.write(htmlEntitiesEncode(cgi.cookies[\"name\"]));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcgi.write(\"friend\");\n\t\t\t\t\t}\n\n\t\t\t\t\t// note that I never called cgi.setResponseContentType, since the default is text/html.\n\t\t\t\t\t// it doesn't hurt to do it explicitly though, just remember to do it before any cgi.write\n\t\t\t\t\t// calls.\n\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\t// no path matched\n\t\t\t\t\tcgi.setResponseStatus(\"404 Not Found\");\n\t\t\t\t\tcgi.write(\"Resource not found.\", true);\n\t\t\t}\n\t\t}\n\n\t\t// and this adds the boilerplate to set up a server according to the\n\t\t// compile version configuration and call your handler as requests come in\n\t\tmixin GenericMain!handler; // the `handler` here is the name of your function\n\t\t---\n\n\tEven 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.\n\n\tIn 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.)\n\n\tA basic program using the higher-level apis might look like:\n\n\t\t---\n\t\t/+\n\t\timport arsd.cgi;\n\n\t\tstruct LoginData {\n\t\t\tstring currentUser;\n\t\t}\n\n\t\tclass AppClass : WebObject {\n\t\t\tstring foo() {}\n\t\t}\n\n\t\tmixin DispatcherMain!(\n\t\t\t\"/assets/.serveStaticFileDirectory(\"assets/\", true), // serve the files in the assets subdirectory\n\t\t\t\"/\".serveApi!AppClass,\n\t\t\t\"/thing/\".serveRestObject,\n\t\t);\n\t\t+/\n\t\t---\n\n\tGuide_for_PHP_users:\n\t\t(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.)\n\n\t\tIf you are coming from old-style PHP, here's a quick guide to help you get started:\n\n\t\t$(SIDE_BY_SIDE\n\t\t\t$(COLUMN\n\t\t\t\t```php\n\t\t\t\t<?php\n\t\t\t\t\t$foo = $_POST[\"foo\"];\n\t\t\t\t\t$bar = $_GET[\"bar\"];\n\t\t\t\t\t$baz = $_COOKIE[\"baz\"];\n\n\t\t\t\t\t$user_ip = $_SERVER[\"REMOTE_ADDR\"];\n\t\t\t\t\t$host = $_SERVER[\"HTTP_HOST\"];\n\t\t\t\t\t$path = $_SERVER[\"PATH_INFO\"];\n\n\t\t\t\t\tsetcookie(\"baz\", \"some value\");\n\n\t\t\t\t\techo \"hello!\";\n\t\t\t\t?>\n\t\t\t\t```\n\t\t\t)\n\t\t\t$(COLUMN\n\t\t\t\t---\n\t\t\t\timport arsd.cgi;\n\t\t\t\tvoid app(Cgi cgi) {\n\t\t\t\t\tstring foo = cgi.post[\"foo\"];\n\t\t\t\t\tstring bar = cgi.get[\"bar\"];\n\t\t\t\t\tstring baz = cgi.cookies[\"baz\"];\n\n\t\t\t\t\tstring user_ip = cgi.remoteAddress;\n\t\t\t\t\tstring host = cgi.host;\n\t\t\t\t\tstring path = cgi.pathInfo;\n\n\t\t\t\t\tcgi.setCookie(\"baz\", \"some value\");\n\n\t\t\t\t\tcgi.write(\"hello!\");\n\t\t\t\t}\n\n\t\t\t\tmixin GenericMain!app\n\t\t\t\t---\n\t\t\t)\n\t\t)\n\n\t\t$(H3 Array elements)\n\n\n\t\tIn PHP, you can give a form element a name like `\"something[]\"`, and then\n\t\t`$_POST[\"something\"]` gives an array. In D, you can use whatever name\n\t\tyou want, and access an array of values with the `cgi.getArray[\"name\"]` and\n\t\t`cgi.postArray[\"name\"]` members.\n\n\t\t$(H3 Databases)\n\n\t\tPHP has a lot of stuff in its standard library. cgi.d doesn't include most\n\t\tof these, but the rest of my arsd repository has much of it. For example,\n\t\tto access a MySQL database, download `database.d` and `mysql.d` from my\n\t\tgithub repo, and try this code (assuming, of course, your database is\n\t\tset up):\n\n\t\t---\n\t\timport arsd.cgi;\n\t\timport arsd.mysql;\n\n\t\tvoid app(Cgi cgi) {\n\t\t\tauto database = new MySql(\"localhost\", \"username\", \"password\", \"database_name\");\n\t\t\tforeach(row; mysql.query(\"SELECT count(id) FROM people\"))\n\t\t\t\tcgi.write(row[0] ~ \" people in database\");\n\t\t}\n\n\t\tmixin GenericMain!app;\n\t\t---\n\n\t\tSimilar modules are available for PostgreSQL, Microsoft SQL Server, and SQLite databases,\n\t\timplementing the same basic interface.\n\n\tSee_Also:\n\n\tYou may also want to see [arsd.dom], [arsd.webtemplate], and maybe some functions from my old [arsd.html] for more code for making\n\tweb applications. dom and webtemplate are used by the higher-level api here in cgi.d.\n\n\tFor working with json, try [arsd.jsvar].\n\n\t[arsd.database], [arsd.mysql], [arsd.postgres], [arsd.mssql], and [arsd.sqlite] can help in\n\taccessing databases.\n\n\tIf you are looking to access a web application via HTTP, try [arsd.http2].\n\n\tCopyright:\n\n\tcgi.d copyright 2008-2023, Adam D. Ruppe. Provided under the Boost Software License.\n\n\tYes, this file is old, and yes, it is still actively maintained and used.\n+/\nmodule arsd.cgi;\n\n// FIXME: Nullable!T can be a checkbox that enables/disables the T on the automatic form\n// 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\n\n/++\n\tThis micro-example uses the [dispatcher] api to act as a simple http file server, serving files found in the current directory and its children.\n+/\nunittest {\n\timport arsd.cgi;\n\n\tmixin DispatcherMain!(\n\t\t\"/\".serveStaticFileDirectory(null, true)\n\t);\n}\n\n/++\n\tSame as the previous example, but written out long-form without the use of [DispatcherMain] nor [GenericMain].\n+/\nunittest {\n\timport arsd.cgi;\n\n\tvoid requestHandler(Cgi cgi) {\n\t\tcgi.dispatcher!(\n\t\t\t\"/\".serveStaticFileDirectory(null, true)\n\t\t);\n\t}\n\n\t// mixin GenericMain!requestHandler would add this function:\n\tvoid main(string[] args) {\n\t\t// this is all the content of [cgiMainImpl] which you can also call\n\n\t\t// cgi.d embeds a few add on functions like real time event forwarders\n\t\t// and session servers it can run in other processes. this spawns them, if needed.\n\t\tif(tryAddonServers(args))\n\t\t\treturn;\n\n\t\t// cgi.d allows you to easily simulate http requests from the command line,\n\t\t// without actually starting a server. this function will do that.\n\t\tif(trySimulatedRequest!(requestHandler, Cgi)(args))\n\t\t\treturn;\n\n\t\tRequestServer server;\n\t\t// you can change the default port here if you like\n\t\t// server.listeningPort = 9000;\n\n\t\t// then call this to let the command line args override your default\n\t\tserver.configureFromCommandLine(args);\n\n\t\t// here is where you could print out the listeningPort to the user if you wanted\n\n\t\t// and serve the request(s) according to the compile configuration\n\t\tserver.serve!(requestHandler)();\n\n\t\t// or you could explicitly choose a serve mode like this:\n\t\t// server.serveEmbeddedHttp!requestHandler();\n\t}\n}\n\n/++\n\t cgi.d has built-in testing helpers too. These will provide mock requests and mock sessions that\n\t otherwise run through the rest of the internal mechanisms to call your functions without actually\n\t spinning up a server.\n+/\nunittest {\n\timport arsd.cgi;\n\n\tvoid requestHandler(Cgi cgi) {\n\n\t}\n\n\t// D doesn't let me embed a unittest inside an example unittest\n\t// so this is a function, but you can do it however in your real program\n\t/* unittest */ void runTests() {\n\t\tauto tester = new CgiTester(&requestHandler);\n\n\t\tauto response = tester.GET(\"/\");\n\t\tassert(response.code == 200);\n\t}\n}\n\nstatic import std.file;\n\n// for a single thread, linear request thing, use:\n// -version=embedded_httpd_threads -version=cgi_no_threads\n\nversion(Posix) {\n\tversion(CRuntime_Musl) {\n\n\t} else version(minimal) {\n\n\t} else {\n\t\tversion(GNU) {\n\t\t\t// GDC doesn't support static foreach so I had to cheat on it :(\n\t\t} else version(FreeBSD) {\n\t\t\t// I never implemented the fancy stuff there either\n\t\t} else version(OpenBSD) {\n\t\t\t// Fix issue #2977 - adopt same approach as FreeBSD above\n\t\t} else {\n\t\t\tversion=with_breaking_cgi_features;\n\t\t\tversion=with_sendfd;\n\t\t\tversion=with_addon_servers;\n\t\t}\n\t}\n}\n\nversion(Windows) {\n\tversion(minimal) {\n\n\t} else {\n\t\t// not too concerned about gdc here since the mingw version is fairly new as well\n\t\tversion=with_breaking_cgi_features;\n\t}\n}\n\nvoid cloexec(int fd) {\n\tversion(Posix) {\n\t\timport core.sys.posix.fcntl;\n\t\tfcntl(fd, F_SETFD, FD_CLOEXEC);\n\t}\n}\n\nvoid cloexec(Socket s) {\n\tversion(Posix) {\n\t\timport core.sys.posix.fcntl;\n\t\tfcntl(s.handle, F_SETFD, FD_CLOEXEC);\n\t}\n}\n\nversion(embedded_httpd_hybrid) {\n\tversion=embedded_httpd_threads;\n\tversion(cgi_no_fork) {} else version(Posix)\n\t\tversion=cgi_use_fork;\n\tversion=cgi_use_fiber;\n}\n\nversion(cgi_use_fork)\n\tenum cgi_use_fork_default = true;\nelse\n\tenum cgi_use_fork_default = false;\n\n// the servers must know about the connections to talk to them; the interfaces are vital\nversion(with_addon_servers)\n\tversion=with_addon_servers_connections;\n\nversion(embedded_httpd) {\n\tversion(linux)\n\t\tversion=embedded_httpd_processes;\n\telse {\n\t\tversion=embedded_httpd_threads;\n\t}\n\n\t/*\n\tversion(with_openssl) {\n\t\tpragma(lib, \"crypto\");\n\t\tpragma(lib, \"ssl\");\n\t}\n\t*/\n}\n\nversion(embedded_httpd_processes)\n\tversion=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\n\nversion(embedded_httpd_threads) {\n\t//  unless the user overrides the default..\n\tversion(cgi_session_server_process)\n\t\t{}\n\telse\n\t\tversion=cgi_embedded_sessions;\n}\nversion(scgi) {\n\t//  unless the user overrides the default..\n\tversion(cgi_session_server_process)\n\t\t{}\n\telse\n\t\tversion=cgi_embedded_sessions;\n}\n\n// fall back if the other is not defined so we can cleanly version it below\nversion(cgi_embedded_sessions) {}\nelse version=cgi_session_server_process;\n\n\nversion=cgi_with_websocket;\n\nenum long defaultMaxContentLength = 5_000_000;\n\n/*\n\n\tTo do a file download offer in the browser:\n\n    cgi.setResponseContentType(\"text/csv\");\n    cgi.header(\"Content-Disposition: attachment; filename=\\\"customers.csv\\\"\");\n*/\n\n// FIXME: the location header is supposed to be an absolute url I guess.\n\n// FIXME: would be cool to flush part of a dom document before complete\n// somehow in here and dom.d.\n\n\n// these are public so you can mixin GenericMain.\n// FIXME: use a function level import instead!\npublic import std.string;\npublic import std.stdio;\npublic import std.conv;\nimport std.concurrency;\nimport std.uri;\nimport std.uni;\nimport std.algorithm.comparison;\nimport std.algorithm.searching;\nimport std.exception;\nimport std.base64;\nstatic import std.algorithm;\nimport std.datetime;\nimport std.range;\n\nimport std.process;\n\nimport std.zlib;\n\n\nT[] consume(T)(T[] range, int count) {\n\tif(count > range.length)\n\t\tcount = range.length;\n\treturn range[count..$];\n}\n\nint locationOf(T)(T[] data, string item) {\n\tconst(ubyte[]) d = cast(const(ubyte[])) data;\n\tconst(ubyte[]) i = cast(const(ubyte[])) item;\n\n\t// this is a vague sanity check to ensure we aren't getting insanely\n\t// sized input that will infinite loop below. it should never happen;\n\t// even huge file uploads ought to come in smaller individual pieces.\n\tif(d.length > (int.max/2))\n\t\tthrow new Exception(\"excessive block of input\");\n\n\tfor(int a = 0; a < d.length; a++) {\n\t\tif(a + i.length > d.length)\n\t\t\treturn -1;\n\t\tif(d[a..a+i.length] == i)\n\t\t\treturn a;\n\t}\n\n\treturn -1;\n}\n\n/// If you are doing a custom cgi class, mixing this in can take care of\n/// the required constructors for you\nmixin template ForwardCgiConstructors() {\n\tthis(long maxContentLength = defaultMaxContentLength,\n\t\tstring[string] env = null,\n\t\tconst(ubyte)[] delegate() readdata = null,\n\t\tvoid delegate(const(ubyte)[]) _rawDataOutput = null,\n\t\tvoid delegate() _flush = null\n\t\t) { super(maxContentLength, env, readdata, _rawDataOutput, _flush); }\n\n\tthis(string[] args) { super(args); }\n\n\tthis(\n\t\tBufferedInputRange inputData,\n\t\tstring address, ushort _port,\n\t\tint pathInfoStarts = 0,\n\t\tbool _https = false,\n\t\tvoid delegate(const(ubyte)[]) _rawDataOutput = null,\n\t\tvoid delegate() _flush = null,\n\t\t// this pointer tells if the connection is supposed to be closed after we handle this\n\t\tbool* closeConnection = null)\n\t{\n\t\tsuper(inputData, address, _port, pathInfoStarts, _https, _rawDataOutput, _flush, closeConnection);\n\t}\n\n\tthis(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); }\n}\n\n/// thrown when a connection is closed remotely while we waiting on data from it\nclass ConnectionClosedException : Exception {\n\tthis(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {\n\t\tsuper(message, file, line, next);\n\t}\n}\n\n\nversion(Windows) {\n// FIXME: ugly hack to solve stdin exception problems on Windows:\n// reading stdin results in StdioException (Bad file descriptor)\n// this is probably due to https://issues.dlang.org/show_bug.cgi?id=3425\nprivate struct stdin {\n\tstruct ByChunk { // Replicates std.stdio.ByChunk\n\tprivate:\n\t\tubyte[] chunk_;\n\tpublic:\n\t\tthis(size_t size)\n\t\tin {\n\t\t\tassert(size, \"size must be larger than 0\");\n\t\t}\n\t\tdo {\n\t\t\tchunk_ = new ubyte[](size);\n\t\t\tpopFront();\n\t\t}\n\n\t\t@property bool empty() const {\n\t\t\treturn !std.stdio.stdin.isOpen || std.stdio.stdin.eof; // Ugly, but seems to do the job\n\t\t}\n\t\t@property nothrow ubyte[] front() {\treturn chunk_; }\n\t\tvoid popFront()\t{\n\t\t\tenforce(!empty, \"Cannot call popFront on empty range\");\n\t\t\tchunk_ = stdin.rawRead(chunk_);\n\t\t}\n\t}\n\n\timport core.sys.windows.windows;\nstatic:\n\n\tT[] rawRead(T)(T[] buf) {\n\t\tuint bytesRead;\n\t\tauto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, cast(int) (buf.length * T.sizeof), &bytesRead, null);\n\n\t\tif (!result) {\n\t\t\tauto err = GetLastError();\n\t\t\tif (err == 38/*ERROR_HANDLE_EOF*/ || err == 109/*ERROR_BROKEN_PIPE*/) // 'good' errors meaning end of input\n\t\t\t\treturn buf[0..0];\n\t\t\t// Some other error, throw it\n\n\t\t\tchar* buffer;\n\t\t\tscope(exit) LocalFree(buffer);\n\n\t\t\t// FORMAT_MESSAGE_ALLOCATE_BUFFER\t= 0x00000100\n\t\t\t// FORMAT_MESSAGE_FROM_SYSTEM\t\t= 0x00001000\n\t\t\tFormatMessageA(0x1100, null, err, 0, cast(char*)&buffer, 256, null);\n\t\t\tthrow new Exception(to!string(buffer));\n\t\t}\n\t\tenforce(!(bytesRead % T.sizeof), \"I/O error\");\n\t\treturn buf[0..bytesRead / T.sizeof];\n\t}\n\n\tauto byChunk(size_t sz) { return ByChunk(sz); }\n\n\tvoid close() {\n\t\tstd.stdio.stdin.close;\n\t}\n}\n}\n\n/// The main interface with the web request\nclass Cgi {\n  public:\n\t/// the methods a request can be\n\tenum RequestMethod { GET, HEAD, POST, PUT, DELETE, // GET and POST are the ones that really work\n\t\t// these are defined in the standard, but idk if they are useful for anything\n\t\tOPTIONS, TRACE, CONNECT,\n\t\t// These seem new, I have only recently seen them\n\t\tPATCH, MERGE,\n\t\t// this is an extension for when the method is not specified and you want to assume\n\t\tCommandLine }\n\n\n\t/+\n\t/++\n\t\tCgi provides a per-request memory pool\n\n\t+/\n\tvoid[] allocateMemory(size_t nBytes) {\n\n\t}\n\n\t/// ditto\n\tvoid[] reallocateMemory(void[] old, size_t nBytes) {\n\n\t}\n\n\t/// ditto\n\tvoid freeMemory(void[] memory) {\n\n\t}\n\t+/\n\n\n/*\n\timport core.runtime;\n\tauto args = Runtime.args();\n\n\twe can call the app a few ways:\n\n\t1) set up the environment variables and call the app (manually simulating CGI)\n\t2) simulate a call automatically:\n\t\t./app method 'uri'\n\n\t\tfor example:\n\t\t\t./app get /path?arg arg2=something\n\n\t  Anything on the uri is treated as query string etc\n\n\t  on get method, further args are appended to the query string (encoded automatically)\n\t  on post method, further args are done as post\n\n\n\t  @name means import from file \"name\". if name == -, it uses stdin\n\t  (so info=@- means set info to the value of stdin)\n\n\n\t  Other arguments include:\n\t  \t--cookie name=value (these are all concated together)\n\t\t--header 'X-Something: cool'\n\t\t--referrer 'something'\n\t\t--port 80\n\t\t--remote-address some.ip.address.here\n\t\t--https yes\n\t\t--user-agent 'something'\n\t\t--userpass 'user:pass'\n\t\t--authorization 'Basic base64encoded_user:pass'\n\t\t--accept 'content' // FIXME: better example\n\t\t--last-event-id 'something'\n\t\t--host 'something.com'\n\n\t  Non-simulation arguments:\n\t  \t--port xxx listening port for non-cgi things (valid for the cgi interfaces)\n\t\t--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`.\n\n*/\n\n\t/** Initializes it with command line arguments (for easy testing) */\n\tthis(string[] args, void delegate(const(ubyte)[]) _rawDataOutput = null) {\n\t\trawDataOutput = _rawDataOutput;\n\t\t// these are all set locally so the loop works\n\t\t// without triggering errors in dmd 2.064\n\t\t// we go ahead and set them at the end of it to the this version\n\t\tint port;\n\t\tstring referrer;\n\t\tstring remoteAddress;\n\t\tstring userAgent;\n\t\tstring authorization;\n\t\tstring origin;\n\t\tstring accept;\n\t\tstring lastEventId;\n\t\tbool https;\n\t\tstring host;\n\t\tRequestMethod requestMethod;\n\t\tstring requestUri;\n\t\tstring pathInfo;\n\t\tstring queryString;\n\n\t\tbool lookingForMethod;\n\t\tbool lookingForUri;\n\t\tstring nextArgIs;\n\n\t\tstring _cookie;\n\t\tstring _queryString;\n\t\tstring[][string] _post;\n\t\tstring[string] _headers;\n\n\t\tstring[] breakUp(string s) {\n\t\t\tstring k, v;\n\t\t\tauto idx = s.indexOf(\"=\");\n\t\t\tif(idx == -1) {\n\t\t\t\tk = s;\n\t\t\t} else {\n\t\t\t\tk = s[0 .. idx];\n\t\t\t\tv = s[idx + 1 .. $];\n\t\t\t}\n\n\t\t\treturn [k, v];\n\t\t}\n\n\t\tlookingForMethod = true;\n\n\t\tscriptName = args[0];\n\t\tscriptFileName = args[0];\n\n\t\tenvironmentVariables = cast(const) environment.toAA;\n\n\t\tforeach(arg; args[1 .. $]) {\n\t\t\tif(arg.startsWith(\"--\")) {\n\t\t\t\tnextArgIs = arg[2 .. $];\n\t\t\t} else if(nextArgIs.length) {\n\t\t\t\tif (nextArgIs == \"cookie\") {\n\t\t\t\t\tauto info = breakUp(arg);\n\t\t\t\t\tif(_cookie.length)\n\t\t\t\t\t\t_cookie ~= \"; \";\n\t\t\t\t\t_cookie ~= std.uri.encodeComponent(info[0]) ~ \"=\" ~ std.uri.encodeComponent(info[1]);\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"port\") {\n\t\t\t\t\tport = to!int(arg);\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"referrer\") {\n\t\t\t\t\treferrer = arg;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"remote-address\") {\n\t\t\t\t\tremoteAddress = arg;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"user-agent\") {\n\t\t\t\t\tuserAgent = arg;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"authorization\") {\n\t\t\t\t\tauthorization = arg;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"userpass\") {\n\t\t\t\t\tauthorization = \"Basic \" ~ Base64.encode(cast(immutable(ubyte)[]) (arg)).idup;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"origin\") {\n\t\t\t\t\torigin = arg;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"accept\") {\n\t\t\t\t\taccept = arg;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"last-event-id\") {\n\t\t\t\t\tlastEventId = arg;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"https\") {\n\t\t\t\t\tif(arg == \"yes\")\n\t\t\t\t\t\thttps = true;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"header\") {\n\t\t\t\t\tstring thing, other;\n\t\t\t\t\tauto idx = arg.indexOf(\":\");\n\t\t\t\t\tif(idx == -1)\n\t\t\t\t\t\tthrow new Exception(\"need a colon in a http header\");\n\t\t\t\t\tthing = arg[0 .. idx];\n\t\t\t\t\tother = arg[idx + 1.. $];\n\t\t\t\t\t_headers[thing.strip.toLower()] = other.strip;\n\t\t\t\t}\n\t\t\t\telse if (nextArgIs == \"host\") {\n\t\t\t\t\thost = arg;\n\t\t\t\t}\n\t\t\t\t// else\n\t\t\t\t// skip, we don't know it but that's ok, it might be used elsewhere so no error\n\n\t\t\t\tnextArgIs = null;\n\t\t\t} else if(lookingForMethod) {\n\t\t\t\tlookingForMethod = false;\n\t\t\t\tlookingForUri = true;\n\n\t\t\t\tif(arg.asLowerCase().equal(\"commandline\"))\n\t\t\t\t\trequestMethod = RequestMethod.CommandLine;\n\t\t\t\telse\n\t\t\t\t\trequestMethod = to!RequestMethod(arg.toUpper());\n\t\t\t} else if(lookingForUri) {\n\t\t\t\tlookingForUri = false;\n\n\t\t\t\trequestUri = arg;\n\n\t\t\t\tauto idx = arg.indexOf(\"?\");\n\t\t\t\tif(idx == -1)\n\t\t\t\t\tpathInfo = arg;\n\t\t\t\telse {\n\t\t\t\t\tpathInfo = arg[0 .. idx];\n\t\t\t\t\t_queryString = arg[idx + 1 .. $];\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// it is an argument of some sort\n\t\t\t\tif(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) {\n\t\t\t\t\tauto parts = breakUp(arg);\n\t\t\t\t\t_post[parts[0]] ~= parts[1];\n\t\t\t\t\tallPostNamesInOrder ~= parts[0];\n\t\t\t\t\tallPostValuesInOrder ~= parts[1];\n\t\t\t\t} else {\n\t\t\t\t\tif(_queryString.length)\n\t\t\t\t\t\t_queryString ~= \"&\";\n\t\t\t\t\tauto parts = breakUp(arg);\n\t\t\t\t\t_queryString ~= std.uri.encodeComponent(parts[0]) ~ \"=\" ~ std.uri.encodeComponent(parts[1]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tacceptsGzip = false;\n\t\tkeepAliveRequested = false;\n\t\trequestHeaders = cast(immutable) _headers;\n\n\t\tcookie = _cookie;\n\t\tcookiesArray =  getCookieArray();\n\t\tcookies = keepLastOf(cookiesArray);\n\n\t\tqueryString = _queryString;\n\t\tgetArray = cast(immutable) decodeVariables(queryString, \"&\", &allGetNamesInOrder, &allGetValuesInOrder);\n\t\tget = keepLastOf(getArray);\n\n\t\tpostArray = cast(immutable) _post;\n\t\tpost = keepLastOf(_post);\n\n\t\t// FIXME\n\t\tfilesArray = null;\n\t\tfiles = null;\n\n\t\tisCalledWithCommandLineArguments = true;\n\n\t\tthis.port = port;\n\t\tthis.referrer = referrer;\n\t\tthis.remoteAddress = remoteAddress;\n\t\tthis.userAgent = userAgent;\n\t\tthis.authorization = authorization;\n\t\tthis.origin = origin;\n\t\tthis.accept = accept;\n\t\tthis.lastEventId = lastEventId;\n\t\tthis.https = https;\n\t\tthis.host = host;\n\t\tthis.requestMethod = requestMethod;\n\t\tthis.requestUri = requestUri;\n\t\tthis.pathInfo = pathInfo;\n\t\tthis.queryString = queryString;\n\t\tthis.postBody = null;\n\t}\n\n\tprivate {\n\t\tstring[] allPostNamesInOrder;\n\t\tstring[] allPostValuesInOrder;\n\t\tstring[] allGetNamesInOrder;\n\t\tstring[] allGetValuesInOrder;\n\t}\n\n\tCgiConnectionHandle getOutputFileHandle() {\n\t\treturn _outputFileHandle;\n\t}\n\n\tCgiConnectionHandle _outputFileHandle = INVALID_CGI_CONNECTION_HANDLE;\n\n\t/** Initializes it using a CGI or CGI-like interface */\n\tthis(long maxContentLength = defaultMaxContentLength,\n\t\t// use this to override the environment variable listing\n\t\tin string[string] env = null,\n\t\t// and this should return a chunk of data. return empty when done\n\t\tconst(ubyte)[] delegate() readdata = null,\n\t\t// finally, use this to do custom output if needed\n\t\tvoid delegate(const(ubyte)[]) _rawDataOutput = null,\n\t\t// to flush the custom output\n\t\tvoid delegate() _flush = null\n\t\t)\n\t{\n\n\t\t// these are all set locally so the loop works\n\t\t// without triggering errors in dmd 2.064\n\t\t// we go ahead and set them at the end of it to the this version\n\t\tint port;\n\t\tstring referrer;\n\t\tstring remoteAddress;\n\t\tstring userAgent;\n\t\tstring authorization;\n\t\tstring origin;\n\t\tstring accept;\n\t\tstring lastEventId;\n\t\tbool https;\n\t\tstring host;\n\t\tRequestMethod requestMethod;\n\t\tstring requestUri;\n\t\tstring pathInfo;\n\t\tstring queryString;\n\n\n\n\t\tisCalledWithCommandLineArguments = false;\n\t\trawDataOutput = _rawDataOutput;\n\t\tflushDelegate = _flush;\n\t\tauto getenv = delegate string(string var) {\n\t\t\tif(env is null)\n\t\t\t\treturn std.process.environment.get(var);\n\t\t\tauto e = var in env;\n\t\t\tif(e is null)\n\t\t\t\treturn null;\n\t\t\treturn *e;\n\t\t};\n\n\t\tenvironmentVariables = env is null ?\n\t\t\tcast(const) environment.toAA :\n\t\t\tenv;\n\n\t\t// fetching all the request headers\n\t\tstring[string] requestHeadersHere;\n\t\tforeach(k, v; env is null ? cast(const) environment.toAA() : env) {\n\t\t\tif(k.startsWith(\"HTTP_\")) {\n\t\t\t\trequestHeadersHere[replace(k[\"HTTP_\".length .. $].toLower(), \"_\", \"-\")] = v;\n\t\t\t}\n\t\t}\n\n\t\tthis.requestHeaders = assumeUnique(requestHeadersHere);\n\n\t\trequestUri = getenv(\"REQUEST_URI\");\n\n\t\tcookie = getenv(\"HTTP_COOKIE\");\n\t\tcookiesArray = getCookieArray();\n\t\tcookies = keepLastOf(cookiesArray);\n\n\t\treferrer = getenv(\"HTTP_REFERER\");\n\t\tuserAgent = getenv(\"HTTP_USER_AGENT\");\n\t\tremoteAddress = getenv(\"REMOTE_ADDR\");\n\t\thost = getenv(\"HTTP_HOST\");\n\t\tpathInfo = getenv(\"PATH_INFO\");\n\n\t\tqueryString = getenv(\"QUERY_STRING\");\n\t\tscriptName = getenv(\"SCRIPT_NAME\");\n\t\t{\n\t\t\timport core.runtime;\n\t\t\tauto sfn = getenv(\"SCRIPT_FILENAME\");\n\t\t\tscriptFileName = sfn.length ? sfn : (Runtime.args.length ? Runtime.args[0] : null);\n\t\t}\n\n\t\tbool iis = false;\n\n\t\t// Because IIS doesn't pass requestUri, we simulate it here if it's empty.\n\t\tif(requestUri.length == 0) {\n\t\t\t// IIS sometimes includes the script name as part of the path info - we don't want that\n\t\t\tif(pathInfo.length >= scriptName.length && (pathInfo[0 .. scriptName.length] == scriptName))\n\t\t\t\tpathInfo = pathInfo[scriptName.length .. $];\n\n\t\t\trequestUri = scriptName ~ pathInfo ~ (queryString.length ? (\"?\" ~ queryString) : \"\");\n\n\t\t\tiis = true; // FIXME HACK - used in byChunk below - see bugzilla 6339\n\n\t\t\t// FIXME: this works for apache and iis... but what about others?\n\t\t}\n\n\n\t\tauto ugh = decodeVariables(queryString, \"&\", &allGetNamesInOrder, &allGetValuesInOrder);\n\t\tgetArray = assumeUnique(ugh);\n\t\tget = keepLastOf(getArray);\n\n\n\t\t// NOTE: on apache, you need to specifically forward this\n\t\tauthorization = getenv(\"HTTP_AUTHORIZATION\");\n\t\t// this is a hack because Apache is a shitload of fuck and\n\t\t// refuses to send the real header to us. Compatible\n\t\t// programs should send both the standard and X- versions\n\n\t\t// NOTE: if you have access to .htaccess or httpd.conf, you can make this\n\t\t// unnecessary with mod_rewrite, so it is commented\n\n\t\t//if(authorization.length == 0) // if the std is there, use it\n\t\t//\tauthorization = getenv(\"HTTP_X_AUTHORIZATION\");\n\n\t\t// the REDIRECT_HTTPS check is here because with an Apache hack, the port can become wrong\n\t\tif(getenv(\"SERVER_PORT\").length && getenv(\"REDIRECT_HTTPS\") != \"on\")\n\t\t\tport = to!int(getenv(\"SERVER_PORT\"));\n\t\telse\n\t\t\tport = 0; // this was probably called from the command line\n\n\t\tauto ae = getenv(\"HTTP_ACCEPT_ENCODING\");\n\t\tif(ae.length && ae.indexOf(\"gzip\") != -1)\n\t\t\tacceptsGzip = true;\n\n\t\taccept = getenv(\"HTTP_ACCEPT\");\n\t\tlastEventId = getenv(\"HTTP_LAST_EVENT_ID\");\n\n\t\tauto ka = getenv(\"HTTP_CONNECTION\");\n\t\tif(ka.length && ka.asLowerCase().canFind(\"keep-alive\"))\n\t\t\tkeepAliveRequested = true;\n\n\t\tauto or = getenv(\"HTTP_ORIGIN\");\n\t\t\torigin = or;\n\n\t\tauto rm = getenv(\"REQUEST_METHOD\");\n\t\tif(rm.length)\n\t\t\trequestMethod = to!RequestMethod(getenv(\"REQUEST_METHOD\"));\n\t\telse\n\t\t\trequestMethod = RequestMethod.CommandLine;\n\n\t\t\t\t\t\t// 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.\n\t\thttps = (getenv(\"HTTPS\") == \"on\" || getenv(\"REDIRECT_HTTPS\") == \"on\");\n\n\t\t// FIXME: DOCUMENT_ROOT?\n\n\t\t// FIXME: what about PUT?\n\t\tif(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) {\n\t\t\tversion(preserveData) // a hack to make forwarding simpler\n\t\t\t\timmutable(ubyte)[] data;\n\t\t\tsize_t amountReceived = 0;\n\t\t\tauto contentType = getenv(\"CONTENT_TYPE\");\n\n\t\t\t// FIXME: is this ever not going to be set? I guess it depends\n\t\t\t// on if the server de-chunks and buffers... seems like it has potential\n\t\t\t// to be slow if they did that. The spec says it is always there though.\n\t\t\t// And it has worked reliably for me all year in the live environment,\n\t\t\t// but some servers might be different.\n\t\t\tauto cls = getenv(\"CONTENT_LENGTH\");\n\t\t\tauto contentLength = to!size_t(cls.length ? cls : \"0\");\n\n\t\t\timmutable originalContentLength = contentLength;\n\t\t\tif(contentLength) {\n\t\t\t\tif(maxContentLength > 0 && contentLength > maxContentLength) {\n\t\t\t\t\tsetResponseStatus(\"413 Request entity too large\");\n\t\t\t\t\twrite(\"You tried to upload a file that is too large.\");\n\t\t\t\t\tclose();\n\t\t\t\t\tthrow new Exception(\"POST too large\");\n\t\t\t\t}\n\t\t\t\tprepareForIncomingDataChunks(contentType, contentLength);\n\n\n\t\t\t\tint processChunk(in ubyte[] chunk) {\n\t\t\t\t\tif(chunk.length > contentLength) {\n\t\t\t\t\t\thandleIncomingDataChunk(chunk[0..contentLength]);\n\t\t\t\t\t\tamountReceived += contentLength;\n\t\t\t\t\t\tcontentLength = 0;\n\t\t\t\t\t\treturn 1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\thandleIncomingDataChunk(chunk);\n\t\t\t\t\t\tcontentLength -= chunk.length;\n\t\t\t\t\t\tamountReceived += chunk.length;\n\t\t\t\t\t}\n\t\t\t\t\tif(contentLength == 0)\n\t\t\t\t\t\treturn 1;\n\n\t\t\t\t\tonRequestBodyDataReceived(amountReceived, originalContentLength);\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\n\t\t\t\tif(readdata is null) {\n\t\t\t\t\tforeach(ubyte[] chunk; stdin.byChunk(iis ? contentLength : 4096))\n\t\t\t\t\t\tif(processChunk(chunk))\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\t// we have a custom data source..\n\t\t\t\t\tauto chunk = readdata();\n\t\t\t\t\twhile(chunk.length) {\n\t\t\t\t\t\tif(processChunk(chunk))\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tchunk = readdata();\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tonRequestBodyDataReceived(amountReceived, originalContentLength);\n\t\t\t\tpostArray = assumeUnique(pps._post);\n\t\t\t\tfilesArray = assumeUnique(pps._files);\n\t\t\t\tfiles = keepLastOf(filesArray);\n\t\t\t\tpost = keepLastOf(postArray);\n\t\t\t\tthis.postBody = pps.postBody;\n\t\t\t\tcleanUpPostDataState();\n\t\t\t}\n\n\t\t\tversion(preserveData)\n\t\t\t\toriginalPostData = data;\n\t\t}\n\t\t// fixme: remote_user script name\n\n\n\t\tthis.port = port;\n\t\tthis.referrer = referrer;\n\t\tthis.remoteAddress = remoteAddress;\n\t\tthis.userAgent = userAgent;\n\t\tthis.authorization = authorization;\n\t\tthis.origin = origin;\n\t\tthis.accept = accept;\n\t\tthis.lastEventId = lastEventId;\n\t\tthis.https = https;\n\t\tthis.host = host;\n\t\tthis.requestMethod = requestMethod;\n\t\tthis.requestUri = requestUri;\n\t\tthis.pathInfo = pathInfo;\n\t\tthis.queryString = queryString;\n\t}\n\n\t/// Cleans up any temporary files. Do not use the object\n\t/// after calling this.\n\t///\n\t/// NOTE: it is called automatically by GenericMain\n\t// FIXME: this should be called if the constructor fails too, if it has created some garbage...\n\tvoid dispose() {\n\t\tforeach(file; files) {\n\t\t\tif(!file.contentInMemory)\n\t\t\t\tif(std.file.exists(file.contentFilename))\n\t\t\t\t\tstd.file.remove(file.contentFilename);\n\t\t}\n\t}\n\n\tprivate {\n\t\tstruct PostParserState {\n\t\t\tstring contentType;\n\t\t\tstring boundary;\n\t\t\tstring localBoundary; // the ones used at the end or something lol\n\t\t\tbool isMultipart;\n\t\t\tbool needsSavedBody;\n\n\t\t\tulong expectedLength;\n\t\t\tulong contentConsumed;\n\t\t\timmutable(ubyte)[] buffer;\n\n\t\t\t// multipart parsing state\n\t\t\tint whatDoWeWant;\n\t\t\tbool weHaveAPart;\n\t\t\tstring[] thisOnesHeaders;\n\t\t\timmutable(ubyte)[] thisOnesData;\n\n\t\t\tstring postBody;\n\n\t\t\tUploadedFile piece;\n\t\t\tbool isFile = false;\n\n\t\t\tsize_t memoryCommitted;\n\n\t\t\t// do NOT keep mutable references to these anywhere!\n\t\t\t// I assume they are unique in the constructor once we're all done getting data.\n\t\t\tstring[][string] _post;\n\t\t\tUploadedFile[][string] _files;\n\t\t}\n\n\t\tPostParserState pps;\n\t}\n\n\t/// This represents a file the user uploaded via a POST request.\n\tstatic struct UploadedFile {\n\t\t/// If you want to create one of these structs for yourself from some data,\n\t\t/// use this function.\n\t\tstatic UploadedFile fromData(immutable(void)[] data, string name = null) {\n\t\t\tCgi.UploadedFile f;\n\t\t\tf.filename = name;\n\t\t\tf.content = cast(immutable(ubyte)[]) data;\n\t\t\tf.contentInMemory = true;\n\t\t\treturn f;\n\t\t}\n\n\t\tstring name; \t\t/// The name of the form element.\n\t\tstring filename; \t/// The filename the user set.\n\t\tstring contentType; \t/// The MIME type the user's browser reported. (Not reliable.)\n\n\t\t/**\n\t\t\tFor small files, cgi.d will buffer the uploaded file in memory, and make it\n\t\t\tdirectly accessible to you through the content member. I find this very convenient\n\t\t\tand somewhat efficient, since it can avoid hitting the disk entirely. (I\n\t\t\toften want to inspect and modify the file anyway!)\n\n\t\t\tI find the file is very large, it is undesirable to eat that much memory just\n\t\t\tfor a file buffer. In those cases, if you pass a large enough value for maxContentLength\n\t\t\tto the constructor so they are accepted, cgi.d will write the content to a temporary\n\t\t\tfile that you can re-read later.\n\n\t\t\tYou can override this behavior by subclassing Cgi and overriding the protected\n\t\t\thandlePostChunk method. Note that the object is not initialized when you\n\t\t\twrite that method - the http headers are available, but the cgi.post method\n\t\t\tis not. You may parse the file as it streams in using this method.\n\n\n\t\t\tAnyway, if the file is small enough to be in memory, contentInMemory will be\n\t\t\tset to true, and the content is available in the content member.\n\n\t\t\tIf not, contentInMemory will be set to false, and the content saved in a file,\n\t\t\twhose name will be available in the contentFilename member.\n\n\n\t\t\tTip: if you know you are always dealing with small files, and want the convenience\n\t\t\tof ignoring this member, construct Cgi with a small maxContentLength. Then, if\n\t\t\ta large file comes in, it simply throws an exception (and HTTP error response)\n\t\t\tinstead of trying to handle it.\n\n\t\t\tThe default value of maxContentLength in the constructor is for small files.\n\t\t*/\n\t\tbool contentInMemory = true; // the default ought to always be true\n\t\timmutable(ubyte)[] content; /// The actual content of the file, if contentInMemory == true\n\t\tstring 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.\n\n\t\t///\n\t\tulong fileSize() {\n\t\t\tif(contentInMemory)\n\t\t\t\treturn content.length;\n\t\t\timport std.file;\n\t\t\treturn std.file.getSize(contentFilename);\n\n\t\t}\n\n\t\t///\n\t\tvoid writeToFile(string filenameToSaveTo) const {\n\t\t\timport std.file;\n\t\t\tif(contentInMemory)\n\t\t\t\tstd.file.write(filenameToSaveTo, content);\n\t\t\telse\n\t\t\t\tstd.file.rename(contentFilename, filenameToSaveTo);\n\t\t}\n\t}\n\n\t// given a content type and length, decide what we're going to do with the data..\n\tprotected void prepareForIncomingDataChunks(string contentType, ulong contentLength) {\n\t\tpps.expectedLength = contentLength;\n\n\t\tauto terminator = contentType.indexOf(\";\");\n\t\tif(terminator == -1)\n\t\t\tterminator = contentType.length;\n\n\t\tpps.contentType = contentType[0 .. terminator];\n\t\tauto b = contentType[terminator .. $];\n\t\tif(b.length) {\n\t\t\tauto idx = b.indexOf(\"boundary=\");\n\t\t\tif(idx != -1) {\n\t\t\t\tpps.boundary = b[idx + \"boundary=\".length .. $];\n\t\t\t\tpps.localBoundary = \"\\r\\n--\" ~ pps.boundary;\n\t\t\t}\n\t\t}\n\n\t\t// while a content type SHOULD be sent according to the RFC, it is\n\t\t// not required. We're told we SHOULD guess by looking at the content\n\t\t// but it seems to me that this only happens when it is urlencoded.\n\t\tif(pps.contentType == \"application/x-www-form-urlencoded\" || pps.contentType == \"\") {\n\t\t\tpps.isMultipart = false;\n\t\t\tpps.needsSavedBody = false;\n\t\t} else if(pps.contentType == \"multipart/form-data\") {\n\t\t\tpps.isMultipart = true;\n\t\t\tenforce(pps.boundary.length, \"no boundary\");\n\t\t} else if(pps.contentType == \"text/xml\") { // FIXME: could this be special and load the post params\n\t\t\t// save the body so the application can handle it\n\t\t\tpps.isMultipart = false;\n\t\t\tpps.needsSavedBody = true;\n\t\t} else if(pps.contentType == \"application/json\") { // FIXME: this could prolly try to load post params too\n\t\t\t// save the body so the application can handle it\n\t\t\tpps.needsSavedBody = true;\n\t\t\tpps.isMultipart = false;\n\t\t} else {\n\t\t\t// the rest is 100% handled by the application. just save the body and send it to them\n\t\t\tpps.needsSavedBody = true;\n\t\t\tpps.isMultipart = false;\n\t\t}\n\t}\n\n\t// handles streaming POST data. If you handle some other content type, you should\n\t// override this. If the data isn't the content type you want, you ought to call\n\t// super.handleIncomingDataChunk so regular forms and files still work.\n\n\t// FIXME: I do some copying in here that I'm pretty sure is unnecessary, and the\n\t// file stuff I'm sure is inefficient. But, my guess is the real bottleneck is network\n\t// input anyway, so I'm not going to get too worked up about it right now.\n\tprotected void handleIncomingDataChunk(const(ubyte)[] chunk) {\n\t\tif(chunk.length == 0)\n\t\t\treturn;\n\t\tassert(chunk.length <= 32 * 1024 * 1024); // we use chunk size as a memory constraint thing, so\n\t\t\t\t\t\t\t// if we're passed big chunks, it might throw unnecessarily.\n\t\t\t\t\t\t\t// just pass it smaller chunks at a time.\n\t\tif(pps.isMultipart) {\n\t\t\t// multipart/form-data\n\n\n\t\t\t// FIXME: this might want to be factored out and factorized\n\t\t\t// need to make sure the stream hooks actually work.\n\t\t\tvoid pieceHasNewContent() {\n\t\t\t\t// we just grew the piece's buffer. Do we have to switch to file backing?\n\t\t\t\tif(pps.piece.contentInMemory) {\n\t\t\t\t\tif(pps.piece.content.length <= 10 * 1024 * 1024)\n\t\t\t\t\t\t// meh, I'm ok with it.\n\t\t\t\t\t\treturn;\n\t\t\t\t\telse {\n\t\t\t\t\t\t// this is too big.\n\t\t\t\t\t\tif(!pps.isFile)\n\t\t\t\t\t\t\tthrow new Exception(\"Request entity too large\"); // a variable this big is kinda ridiculous, just reject it.\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t// a file this large is probably acceptable though... let's use a backing file.\n\t\t\t\t\t\t\tpps.piece.contentInMemory = false;\n\t\t\t\t\t\t\t// FIXME: say... how do we intend to delete these things? cgi.dispose perhaps.\n\n\t\t\t\t\t\t\tint count = 0;\n\t\t\t\t\t\t\tpps.piece.contentFilename = getTempDirectory() ~ \"arsd_cgi_uploaded_file_\" ~ to!string(getUtcTime()) ~ \"-\" ~ to!string(count);\n\t\t\t\t\t\t\t// odds are this loop will never be entered, but we want it just in case.\n\t\t\t\t\t\t\twhile(std.file.exists(pps.piece.contentFilename)) {\n\t\t\t\t\t\t\t\tcount++;\n\t\t\t\t\t\t\t\tpps.piece.contentFilename = getTempDirectory() ~ \"arsd_cgi_uploaded_file_\" ~ to!string(getUtcTime()) ~ \"-\" ~ to!string(count);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// I hope this creates the file pretty quickly, or the loop might be useless...\n\t\t\t\t\t\t\t// FIXME: maybe I should write some kind of custom transaction here.\n\t\t\t\t\t\t\tstd.file.write(pps.piece.contentFilename, pps.piece.content);\n\n\t\t\t\t\t\t\tpps.piece.content = null;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// it's already in a file, so just append it to what we have\n\t\t\t\t\tif(pps.piece.content.length) {\n\t\t\t\t\t\t// FIXME: this is surely very inefficient... we'll be calling this by 4kb chunk...\n\t\t\t\t\t\tstd.file.append(pps.piece.contentFilename, pps.piece.content);\n\t\t\t\t\t\tpps.piece.content = null;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\n\t\t\tvoid commitPart() {\n\t\t\t\tif(!pps.weHaveAPart)\n\t\t\t\t\treturn;\n\n\t\t\t\tpieceHasNewContent(); // be sure the new content is handled every time\n\n\t\t\t\tif(pps.isFile) {\n\t\t\t\t\t// I'm not sure if other environments put files in post or not...\n\t\t\t\t\t// I used to not do it, but I think I should, since it is there...\n\t\t\t\t\tpps._post[pps.piece.name] ~= pps.piece.filename;\n\t\t\t\t\tpps._files[pps.piece.name] ~= pps.piece;\n\n\t\t\t\t\tallPostNamesInOrder ~= pps.piece.name;\n\t\t\t\t\tallPostValuesInOrder ~= pps.piece.filename;\n\t\t\t\t} else {\n\t\t\t\t\tpps._post[pps.piece.name] ~= cast(string) pps.piece.content;\n\n\t\t\t\t\tallPostNamesInOrder ~= pps.piece.name;\n\t\t\t\t\tallPostValuesInOrder ~= cast(string) pps.piece.content;\n\t\t\t\t}\n\n\t\t\t\t/*\n\t\t\t\tstderr.writeln(\"RECEIVED: \", pps.piece.name, \"=\",\n\t\t\t\t\tpps.piece.content.length < 1000\n\t\t\t\t\t?\n\t\t\t\t\tto!string(pps.piece.content)\n\t\t\t\t\t:\n\t\t\t\t\t\"too long\");\n\t\t\t\t*/\n\n\t\t\t\t// FIXME: the limit here\n\t\t\t\tpps.memoryCommitted += pps.piece.content.length;\n\n\t\t\t\tpps.weHaveAPart = false;\n\t\t\t\tpps.whatDoWeWant = 1;\n\t\t\t\tpps.thisOnesHeaders = null;\n\t\t\t\tpps.thisOnesData = null;\n\n\t\t\t\tpps.piece = UploadedFile.init;\n\t\t\t\tpps.isFile = false;\n\t\t\t}\n\n\t\t\tvoid acceptChunk() {\n\t\t\t\tpps.buffer ~= chunk;\n\t\t\t\tchunk = null; // we've consumed it into the buffer, so keeping it just brings confusion\n\t\t\t}\n\n\t\t\timmutable(ubyte)[] consume(size_t howMuch) {\n\t\t\t\tpps.contentConsumed += howMuch;\n\t\t\t\tauto ret = pps.buffer[0 .. howMuch];\n\t\t\t\tpps.buffer = pps.buffer[howMuch .. $];\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\tdataConsumptionLoop: do {\n\t\t\tswitch(pps.whatDoWeWant) {\n\t\t\t\tdefault: assert(0);\n\t\t\t\tcase 0:\n\t\t\t\t\tacceptChunk();\n\t\t\t\t\t// the format begins with two extra leading dashes, then we should be at the boundary\n\t\t\t\t\tif(pps.buffer.length < 2)\n\t\t\t\t\t\treturn;\n\t\t\t\t\tassert(pps.buffer[0] == '-', \"no leading dash\");\n\t\t\t\t\tconsume(1);\n\t\t\t\t\tassert(pps.buffer[0] == '-', \"no second leading dash\");\n\t\t\t\t\tconsume(1);\n\n\t\t\t\t\tpps.whatDoWeWant = 1;\n\t\t\t\t\tgoto case 1;\n\t\t\t\t/* fallthrough */\n\t\t\t\tcase 1: // looking for headers\n\t\t\t\t\t// here, we should be lined up right at the boundary, which is followed by a \\r\\n\n\n\t\t\t\t\t// want to keep the buffer under control in case we're under attack\n\t\t\t\t\t//stderr.writeln(\"here once\");\n\t\t\t\t\t//if(pps.buffer.length + chunk.length > 70 * 1024) // they should be < 1 kb really....\n\t\t\t\t\t//\tthrow new Exception(\"wtf is up with the huge mime part headers\");\n\n\t\t\t\t\tacceptChunk();\n\n\t\t\t\t\tif(pps.buffer.length < pps.boundary.length)\n\t\t\t\t\t\treturn; // not enough data, since there should always be a boundary here at least\n\n\t\t\t\t\tif(pps.contentConsumed + pps.boundary.length + 6 == pps.expectedLength) {\n\t\t\t\t\t\tassert(pps.buffer.length == pps.boundary.length + 4 + 2); // --, --, and \\r\\n\n\t\t\t\t\t\t// we *should* be at the end here!\n\t\t\t\t\t\tassert(pps.buffer[0] == '-');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t\tassert(pps.buffer[0] == '-');\n\t\t\t\t\t\tconsume(1);\n\n\t\t\t\t\t\t// the message is terminated by --BOUNDARY--\\r\\n (after a \\r\\n leading to the boundary)\n\t\t\t\t\t\tassert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary,\n\t\t\t\t\t\t\t\"not lined up on boundary \" ~ pps.boundary);\n\t\t\t\t\t\tconsume(pps.boundary.length);\n\n\t\t\t\t\t\tassert(pps.buffer[0] == '-');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t\tassert(pps.buffer[0] == '-');\n\t\t\t\t\t\tconsume(1);\n\n\t\t\t\t\t\tassert(pps.buffer[0] == '\\r');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t\tassert(pps.buffer[0] == '\\n');\n\t\t\t\t\t\tconsume(1);\n\n\t\t\t\t\t\tassert(pps.buffer.length == 0);\n\t\t\t\t\t\tassert(pps.contentConsumed == pps.expectedLength);\n\t\t\t\t\t\tbreak dataConsumptionLoop; // we're done!\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// we're not done yet. We should be lined up on a boundary.\n\n\t\t\t\t\t\t// But, we want to ensure the headers are here before we consume anything!\n\t\t\t\t\t\tauto headerEndLocation = locationOf(pps.buffer, \"\\r\\n\\r\\n\");\n\t\t\t\t\t\tif(headerEndLocation == -1)\n\t\t\t\t\t\t\treturn; // they *should* all be here, so we can handle them all at once.\n\n\t\t\t\t\t\tassert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary,\n\t\t\t\t\t\t\t\"not lined up on boundary \" ~ pps.boundary);\n\n\t\t\t\t\t\tconsume(pps.boundary.length);\n\t\t\t\t\t\t// the boundary is always followed by a \\r\\n\n\t\t\t\t\t\tassert(pps.buffer[0] == '\\r');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t\tassert(pps.buffer[0] == '\\n');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t}\n\n\t\t\t\t\t// re-running since by consuming the boundary, we invalidate the old index.\n\t\t\t\t\tauto headerEndLocation = locationOf(pps.buffer, \"\\r\\n\\r\\n\");\n\t\t\t\t\tassert(headerEndLocation >= 0, \"no header\");\n\t\t\t\t\tauto thisOnesHeaders = pps.buffer[0..headerEndLocation];\n\n\t\t\t\t\tconsume(headerEndLocation + 4); // The +4 is the \\r\\n\\r\\n that caps it off\n\n\t\t\t\t\tpps.thisOnesHeaders = split(cast(string) thisOnesHeaders, \"\\r\\n\");\n\n\t\t\t\t\t// now we'll parse the headers\n\t\t\t\t\tforeach(h; pps.thisOnesHeaders) {\n\t\t\t\t\t\tauto p = h.indexOf(\":\");\n\t\t\t\t\t\tassert(p != -1, \"no colon in header, got \" ~ to!string(pps.thisOnesHeaders));\n\t\t\t\t\t\tstring hn = h[0..p];\n\t\t\t\t\t\tstring hv = h[p+2..$];\n\n\t\t\t\t\t\tswitch(hn.toLower) {\n\t\t\t\t\t\t\tdefault: assert(0);\n\t\t\t\t\t\t\tcase \"content-disposition\":\n\t\t\t\t\t\t\t\tauto info = hv.split(\"; \");\n\t\t\t\t\t\t\t\tforeach(i; info[1..$]) { // skipping the form-data\n\t\t\t\t\t\t\t\t\tauto o = i.split(\"=\"); // FIXME\n\t\t\t\t\t\t\t\t\tstring pn = o[0];\n\t\t\t\t\t\t\t\t\tstring pv = o[1][1..$-1];\n\n\t\t\t\t\t\t\t\t\tif(pn == \"name\") {\n\t\t\t\t\t\t\t\t\t\tpps.piece.name = pv;\n\t\t\t\t\t\t\t\t\t} else if (pn == \"filename\") {\n\t\t\t\t\t\t\t\t\t\tpps.piece.filename = pv;\n\t\t\t\t\t\t\t\t\t\tpps.isFile = true;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tcase \"content-type\":\n\t\t\t\t\t\t\t\tpps.piece.contentType = hv;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tpps.whatDoWeWant++; // move to the next step - the data\n\t\t\t\tbreak;\n\t\t\t\tcase 2:\n\t\t\t\t\t// when we get here, pps.buffer should contain our first chunk of data\n\n\t\t\t\t\tif(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // we might buffer quite a bit but not much\n\t\t\t\t\t\tthrow new Exception(\"wtf is up with the huge mime part buffer\");\n\n\t\t\t\t\tacceptChunk();\n\n\t\t\t\t\t// so the trick is, we want to process all the data up to the boundary,\n\t\t\t\t\t// but what if the chunk's end cuts the boundary off? If we're unsure, we\n\t\t\t\t\t// want to wait for the next chunk. We start by looking for the whole boundary\n\t\t\t\t\t// in the buffer somewhere.\n\n\t\t\t\t\tauto boundaryLocation = locationOf(pps.buffer, pps.localBoundary);\n\t\t\t\t\t// assert(boundaryLocation != -1, \"should have seen \"~to!string(cast(ubyte[]) pps.localBoundary)~\" in \" ~ to!string(pps.buffer));\n\t\t\t\t\tif(boundaryLocation != -1) {\n\t\t\t\t\t\t// this is easy - we can see it in it's entirety!\n\n\t\t\t\t\t\tpps.piece.content ~= consume(boundaryLocation);\n\n\t\t\t\t\t\tassert(pps.buffer[0] == '\\r');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t\tassert(pps.buffer[0] == '\\n');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t\tassert(pps.buffer[0] == '-');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t\tassert(pps.buffer[0] == '-');\n\t\t\t\t\t\tconsume(1);\n\t\t\t\t\t\t// the boundary here is always preceded by \\r\\n--, which is why we used localBoundary instead of boundary to locate it. Cut that off.\n\t\t\t\t\t\tpps.weHaveAPart = true;\n\t\t\t\t\t\tpps.whatDoWeWant = 1; // back to getting headers for the next part\n\n\t\t\t\t\t\tcommitPart(); // we're done here\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// we can't see the whole thing, but what if there's a partial boundary?\n\n\t\t\t\t\t\tenforce(pps.localBoundary.length < 128); // the boundary ought to be less than a line...\n\t\t\t\t\t\tassert(pps.localBoundary.length > 1); // should already be sane but just in case\n\t\t\t\t\t\tbool potentialBoundaryFound = false;\n\n\t\t\t\t\t\tboundaryCheck: for(int a = 1; a < pps.localBoundary.length; a++) {\n\t\t\t\t\t\t\t// we grow the boundary a bit each time. If we think it looks the\n\t\t\t\t\t\t\t// same, better pull another chunk to be sure it's not the end.\n\t\t\t\t\t\t\t// Starting small because exiting the loop early is desirable, since\n\t\t\t\t\t\t\t// we're not keeping any ambiguity and 1 / 256 chance of exiting is\n\t\t\t\t\t\t\t// the best we can do.\n\t\t\t\t\t\t\tif(a > pps.buffer.length)\n\t\t\t\t\t\t\t\tbreak; // FIXME: is this right?\n\t\t\t\t\t\t\tassert(a <= pps.buffer.length);\n\t\t\t\t\t\t\tassert(a > 0);\n\t\t\t\t\t\t\tif(std.algorithm.endsWith(pps.buffer, pps.localBoundary[0 .. a])) {\n\t\t\t\t\t\t\t\t// ok, there *might* be a boundary here, so let's\n\t\t\t\t\t\t\t\t// not treat the end as data yet. The rest is good to\n\t\t\t\t\t\t\t\t// use though, since if there was a boundary there, we'd\n\t\t\t\t\t\t\t\t// have handled it up above after locationOf.\n\n\t\t\t\t\t\t\t\tpps.piece.content ~= pps.buffer[0 .. $ - a];\n\t\t\t\t\t\t\t\tconsume(pps.buffer.length - a);\n\t\t\t\t\t\t\t\tpieceHasNewContent();\n\t\t\t\t\t\t\t\tpotentialBoundaryFound = true;\n\t\t\t\t\t\t\t\tbreak boundaryCheck;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif(!potentialBoundaryFound) {\n\t\t\t\t\t\t\t// we can consume the whole thing\n\t\t\t\t\t\t\tpps.piece.content ~= pps.buffer;\n\t\t\t\t\t\t\tpieceHasNewContent();\n\t\t\t\t\t\t\tconsume(pps.buffer.length);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// we found a possible boundary, but there was\n\t\t\t\t\t\t\t// insufficient data to be sure.\n\t\t\t\t\t\t\tassert(pps.buffer == cast(const(ubyte[])) pps.localBoundary[0 .. pps.buffer.length]);\n\n\t\t\t\t\t\t\treturn; // wait for the next chunk.\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t}\n\t\t\t} while(pps.buffer.length);\n\n\t\t\t// btw all boundaries except the first should have a \\r\\n before them\n\t\t} else {\n\t\t\t// application/x-www-form-urlencoded and application/json\n\n\t\t\t\t// not using maxContentLength because that might be cranked up to allow\n\t\t\t\t// large file uploads. We can handle them, but a huge post[] isn't any good.\n\t\t\tif(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // surely this is plenty big enough\n\t\t\t\tthrow new Exception(\"wtf is up with such a gigantic form submission????\");\n\n\t\t\tpps.buffer ~= chunk;\n\n\t\t\t// simple handling, but it works... until someone bombs us with gigabytes of crap at least...\n\t\t\tif(pps.buffer.length == pps.expectedLength) {\n\t\t\t\tif(pps.needsSavedBody)\n\t\t\t\t\tpps.postBody = cast(string) pps.buffer;\n\t\t\t\telse\n\t\t\t\t\tpps._post = decodeVariables(cast(string) pps.buffer, \"&\", &allPostNamesInOrder, &allPostValuesInOrder);\n\t\t\t\tversion(preserveData)\n\t\t\t\t\toriginalPostData = pps.buffer;\n\t\t\t} else {\n\t\t\t\t// just for debugging\n\t\t\t}\n\t\t}\n\t}\n\n\tprotected void cleanUpPostDataState() {\n\t\tpps = PostParserState.init;\n\t}\n\n\t/// you can override this function to somehow react\n\t/// to an upload in progress.\n\t///\n\t/// Take note that parts of the CGI object is not yet\n\t/// initialized! Stuff from HTTP headers, including get[], is usable.\n\t/// But, none of post[] is usable, and you cannot write here. That's\n\t/// why this method is const - mutating the object won't do much anyway.\n\t///\n\t/// My idea here was so you can output a progress bar or\n\t/// something to a cooperative client (see arsd.rtud for a potential helper)\n\t///\n\t/// The default is to do nothing. Subclass cgi and use the\n\t/// CustomCgiMain mixin to do something here.\n\tvoid onRequestBodyDataReceived(size_t receivedSoFar, size_t totalExpected) const {\n\t\t// This space intentionally left blank.\n\t}\n\n\t/// Initializes the cgi from completely raw HTTP data. The ir must have a Socket source.\n\t/// *closeConnection will be set to true if you should close the connection after handling this request\n\tthis(BufferedInputRange ir, bool* closeConnection) {\n\t\tisCalledWithCommandLineArguments = false;\n\t\timport al = std.algorithm;\n\n\t\timmutable(ubyte)[] data;\n\n\t\tvoid rdo(const(ubyte)[] d) {\n\t\t//import std.stdio; writeln(d);\n\t\t\tsendAll(ir.source, d);\n\t\t}\n\n\t\tauto ira = ir.source.remoteAddress();\n\t\tauto irLocalAddress = ir.source.localAddress();\n\n\t\tushort port = 80;\n\t\tif(auto ia = cast(InternetAddress) irLocalAddress) {\n\t\t\tport = ia.port;\n\t\t} else if(auto ia = cast(Internet6Address) irLocalAddress) {\n\t\t\tport = ia.port;\n\t\t}\n\n\t\t// that check for UnixAddress is to work around a Phobos bug\n\t\t// see: https://github.com/dlang/phobos/pull/7383\n\t\t// but this might be more useful anyway tbh for this case\n\t\tversion(Posix)\n\t\tthis(ir, ira is null ? null : cast(UnixAddress) ira ? \"unix:\" : ira.toString(), port, 0, false, &rdo, null, closeConnection);\n\t\telse\n\t\tthis(ir, ira is null ? null : ira.toString(), port, 0, false, &rdo, null, closeConnection);\n\t}\n\n\t/**\n\t\tInitializes it from raw HTTP request data. GenericMain uses this when you compile with -version=embedded_httpd.\n\n\t\tNOTE: 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\n\n\t\tParams:\n\t\t\tinputData = the incoming data, including headers and other raw http data.\n\t\t\t\tWhen the constructor exits, it will leave this range exactly at the start of\n\t\t\t\tthe next request on the connection (if there is one).\n\n\t\t\taddress = the IP address of the remote user\n\t\t\t_port = the port number of the connection\n\t\t\tpathInfoStarts = the offset into the path component of the http header where the SCRIPT_NAME ends and the PATH_INFO begins.\n\t\t\t_https = if this connection is encrypted (note that the input data must not actually be encrypted)\n\t\t\t_rawDataOutput = delegate to accept response data. It should write to the socket or whatever; Cgi does all the needed processing to speak http.\n\t\t\t_flush = if _rawDataOutput buffers, this delegate should flush the buffer down the wire\n\t\t\tcloseConnection = if the request asks to close the connection, *closeConnection == true.\n\t*/\n\tthis(\n\t\tBufferedInputRange inputData,\n//\t\tstring[] headers, immutable(ubyte)[] data,\n\t\tstring address, ushort _port,\n\t\tint pathInfoStarts = 0, // use this if you know the script name, like if this is in a folder in a bigger web environment\n\t\tbool _https = false,\n\t\tvoid delegate(const(ubyte)[]) _rawDataOutput = null,\n\t\tvoid delegate() _flush = null,\n\t\t// this pointer tells if the connection is supposed to be closed after we handle this\n\t\tbool* closeConnection = null)\n\t{\n\t\t// these are all set locally so the loop works\n\t\t// without triggering errors in dmd 2.064\n\t\t// we go ahead and set them at the end of it to the this version\n\t\tint port;\n\t\tstring referrer;\n\t\tstring remoteAddress;\n\t\tstring userAgent;\n\t\tstring authorization;\n\t\tstring origin;\n\t\tstring accept;\n\t\tstring lastEventId;\n\t\tbool https;\n\t\tstring host;\n\t\tRequestMethod requestMethod;\n\t\tstring requestUri;\n\t\tstring pathInfo;\n\t\tstring queryString;\n\t\tstring scriptName;\n\t\tstring[string] get;\n\t\tstring[][string] getArray;\n\t\tbool keepAliveRequested;\n\t\tbool acceptsGzip;\n\t\tstring cookie;\n\n\n\n\t\tenvironmentVariables = cast(const) environment.toAA;\n\n\t\tidlol = inputData;\n\n\t\tisCalledWithCommandLineArguments = false;\n\n\t\thttps = _https;\n\t\tport = _port;\n\n\t\trawDataOutput = _rawDataOutput;\n\t\tflushDelegate = _flush;\n\t\tnph = true;\n\n\t\tremoteAddress = address;\n\n\t\t// streaming parser\n\t\timport al = std.algorithm;\n\n\t\t\t// FIXME: tis cast is technically wrong, but Phobos deprecated al.indexOf... for some reason.\n\t\tauto idx = indexOf(cast(string) inputData.front(), \"\\r\\n\\r\\n\");\n\t\twhile(idx == -1) {\n\t\t\tinputData.popFront(0);\n\t\t\tidx = indexOf(cast(string) inputData.front(), \"\\r\\n\\r\\n\");\n\t\t}\n\n\t\tassert(idx != -1);\n\n\n\t\tstring contentType = \"\";\n\t\tstring[string] requestHeadersHere;\n\n\t\tsize_t contentLength;\n\n\t\tbool isChunked;\n\n\t\t{\n\t\t\timport core.runtime;\n\t\t\tscriptFileName = Runtime.args.length ? Runtime.args[0] : null;\n\t\t}\n\n\n\t\tint headerNumber = 0;\n\t\tforeach(line; al.splitter(inputData.front()[0 .. idx], \"\\r\\n\"))\n\t\tif(line.length) {\n\t\t\theaderNumber++;\n\t\t\tauto header = cast(string) line.idup;\n\t\t\tif(headerNumber == 1) {\n\t\t\t\t// request line\n\t\t\t\tauto parts = al.splitter(header, \" \");\n\t\t\t\trequestMethod = to!RequestMethod(parts.front);\n\t\t\t\tparts.popFront();\n\t\t\t\trequestUri = parts.front;\n\n\t\t\t\t// FIXME:  the requestUri could be an absolute path!!! should I rename it or something?\n\t\t\t\tscriptName = requestUri[0 .. pathInfoStarts];\n\n\t\t\t\tauto question = requestUri.indexOf(\"?\");\n\t\t\t\tif(question == -1) {\n\t\t\t\t\tqueryString = \"\";\n\t\t\t\t\t// FIXME: double check, this might be wrong since it could be url encoded\n\t\t\t\t\tpathInfo = requestUri[pathInfoStarts..$];\n\t\t\t\t} else {\n\t\t\t\t\tqueryString = requestUri[question+1..$];\n\t\t\t\t\tpathInfo = requestUri[pathInfoStarts..question];\n\t\t\t\t}\n\n\t\t\t\tauto ugh = decodeVariables(queryString, \"&\", &allGetNamesInOrder, &allGetValuesInOrder);\n\t\t\t\tgetArray = cast(string[][string]) assumeUnique(ugh);\n\n\t\t\t\tif(header.indexOf(\"HTTP/1.0\") != -1) {\n\t\t\t\t\thttp10 = true;\n\t\t\t\t\tautoBuffer = true;\n\t\t\t\t\tif(closeConnection) {\n\t\t\t\t\t\t// on http 1.0, close is assumed (unlike http/1.1 where we assume keep alive)\n\t\t\t\t\t\t*closeConnection = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// other header\n\t\t\t\tauto colon = header.indexOf(\":\");\n\t\t\t\tif(colon == -1)\n\t\t\t\t\tthrow new Exception(\"HTTP headers should have a colon!\");\n\t\t\t\tstring name = header[0..colon].toLower;\n\t\t\t\tstring value = header[colon+2..$]; // skip the colon and the space\n\n\t\t\t\trequestHeadersHere[name] = value;\n\n\t\t\t\tif (name == \"accept\") {\n\t\t\t\t\taccept = value;\n\t\t\t\t}\n\t\t\t\telse if (name == \"origin\") {\n\t\t\t\t\torigin = value;\n\t\t\t\t}\n\t\t\t\telse if (name == \"connection\") {\n\t\t\t\t\tif(value == \"close\" && closeConnection)\n\t\t\t\t\t\t*closeConnection = true;\n\t\t\t\t\tif(value.asLowerCase().canFind(\"keep-alive\")) {\n\t\t\t\t\t\tkeepAliveRequested = true;\n\n\t\t\t\t\t\t// on http 1.0, the connection is closed by default,\n\t\t\t\t\t\t// but not if they request keep-alive. then we don't close\n\t\t\t\t\t\t// anymore - undoing the set above\n\t\t\t\t\t\tif(http10 && closeConnection) {\n\t\t\t\t\t\t\t*closeConnection = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if (name == \"transfer-encoding\") {\n\t\t\t\t\tif(value == \"chunked\")\n\t\t\t\t\t\tisChunked = true;\n\t\t\t\t}\n\t\t\t\telse if (name == \"last-event-id\") {\n\t\t\t\t\tlastEventId = value;\n\t\t\t\t}\n\t\t\t\telse if (name == \"authorization\") {\n\t\t\t\t\tauthorization = value;\n\t\t\t\t}\n\t\t\t\telse if (name == \"content-type\") {\n\t\t\t\t\tcontentType = value;\n\t\t\t\t}\n\t\t\t\telse if (name == \"content-length\") {\n\t\t\t\t\tcontentLength = to!size_t(value);\n\t\t\t\t}\n\t\t\t\telse if (name == \"x-forwarded-for\") {\n\t\t\t\t\tremoteAddress = value;\n\t\t\t\t}\n\t\t\t\telse if (name == \"x-forwarded-host\" || name == \"host\") {\n\t\t\t\t\tif(name != \"host\" || host is null)\n\t\t\t\t\t\thost = value;\n\t\t\t\t}\n\t\t\t\t// FIXME: https://tools.ietf.org/html/rfc7239\n\t\t\t\telse if (name == \"accept-encoding\") {\n\t\t\t\t\tif(value.indexOf(\"gzip\") != -1)\n\t\t\t\t\t\tacceptsGzip = true;\n\t\t\t\t}\n\t\t\t\telse if (name == \"user-agent\") {\n\t\t\t\t\tuserAgent = value;\n\t\t\t\t}\n\t\t\t\telse if (name == \"referer\") {\n\t\t\t\t\treferrer = value;\n\t\t\t\t}\n\t\t\t\telse if (name == \"cookie\") {\n\t\t\t\t\tcookie ~= value;\n\t\t\t\t} else if(name == \"expect\") {\n\t\t\t\t\tif(value == \"100-continue\") {\n\t\t\t\t\t\t// FIXME we should probably give user code a chance\n\t\t\t\t\t\t// to process and reject but that needs to be virtual,\n\t\t\t\t\t\t// perhaps part of the CGI redesign.\n\n\t\t\t\t\t\t// FIXME: if size is > max content length it should\n\t\t\t\t\t\t// also fail at this point.\n\t\t\t\t\t\t_rawDataOutput(cast(ubyte[]) \"HTTP/1.1 100 Continue\\r\\n\\r\\n\");\n\n\t\t\t\t\t\t// FIXME: let the user write out 103 early hints too\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// else\n\t\t\t\t// ignore it\n\n\t\t\t}\n\t\t}\n\n\t\tinputData.consume(idx + 4);\n\t\t// done\n\n\t\trequestHeaders = assumeUnique(requestHeadersHere);\n\n\t\tByChunkRange dataByChunk;\n\n\t\t// reading Content-Length type data\n\t\t// We need to read up the data we have, and write it out as a chunk.\n\t\tif(!isChunked) {\n\t\t\tdataByChunk = byChunk(inputData, contentLength);\n\t\t} else {\n\t\t\t// chunked requests happen, but not every day. Since we need to know\n\t\t\t// the content length (for now, maybe that should change), we'll buffer\n\t\t\t// the whole thing here instead of parse streaming. (I think this is what Apache does anyway in cgi modes)\n\t\t\tauto data = dechunk(inputData);\n\n\t\t\t// set the range here\n\t\t\tdataByChunk = byChunk(data);\n\t\t\tcontentLength = data.length;\n\t\t}\n\n\t\tassert(dataByChunk !is null);\n\n\t\tif(contentLength) {\n\t\t\tprepareForIncomingDataChunks(contentType, contentLength);\n\t\t\tforeach(dataChunk; dataByChunk) {\n\t\t\t\thandleIncomingDataChunk(dataChunk);\n\t\t\t}\n\t\t\tpostArray = assumeUnique(pps._post);\n\t\t\tfilesArray = assumeUnique(pps._files);\n\t\t\tfiles = keepLastOf(filesArray);\n\t\t\tpost = keepLastOf(postArray);\n\t\t\tpostBody = pps.postBody;\n\t\t\tcleanUpPostDataState();\n\t\t}\n\n\t\tthis.port = port;\n\t\tthis.referrer = referrer;\n\t\tthis.remoteAddress = remoteAddress;\n\t\tthis.userAgent = userAgent;\n\t\tthis.authorization = authorization;\n\t\tthis.origin = origin;\n\t\tthis.accept = accept;\n\t\tthis.lastEventId = lastEventId;\n\t\tthis.https = https;\n\t\tthis.host = host;\n\t\tthis.requestMethod = requestMethod;\n\t\tthis.requestUri = requestUri;\n\t\tthis.pathInfo = pathInfo;\n\t\tthis.queryString = queryString;\n\n\t\tthis.scriptName = scriptName;\n\t\tthis.get = keepLastOf(getArray);\n\t\tthis.getArray = cast(immutable) getArray;\n\t\tthis.keepAliveRequested = keepAliveRequested;\n\t\tthis.acceptsGzip = acceptsGzip;\n\t\tthis.cookie = cookie;\n\n\t\tcookiesArray = getCookieArray();\n\t\tcookies = keepLastOf(cookiesArray);\n\n\t}\n\tBufferedInputRange idlol;\n\n\tprivate immutable(string[string]) keepLastOf(in string[][string] arr) {\n\t\tstring[string] ca;\n\t\tforeach(k, v; arr)\n\t\t\tca[k] = v[$-1];\n\n\t\treturn assumeUnique(ca);\n\t}\n\n\t// FIXME duplication\n\tprivate immutable(UploadedFile[string]) keepLastOf(in UploadedFile[][string] arr) {\n\t\tUploadedFile[string] ca;\n\t\tforeach(k, v; arr)\n\t\t\tca[k] = v[$-1];\n\n\t\treturn assumeUnique(ca);\n\t}\n\n\n\tprivate immutable(string[][string]) getCookieArray() {\n\t\tauto forTheLoveOfGod = decodeVariables(cookie, \"; \");\n\t\treturn assumeUnique(forTheLoveOfGod);\n\t}\n\n\t/// Very simple method to require a basic auth username and password.\n\t/// If the http request doesn't include the required credentials, it throws a\n\t/// HTTP 401 error, and an exception.\n\t///\n\t/// Note: basic auth does not provide great security, especially over unencrypted HTTP;\n\t/// the user's credentials are sent in plain text on every request.\n\t///\n\t/// If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the\n\t/// application. Either use Apache's built in methods for basic authentication, or add\n\t/// something along these lines to your server configuration:\n\t///\n\t///      RewriteEngine On\n\t///      RewriteCond %{HTTP:Authorization} ^(.*)\n\t///      RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]\n\t///\n\t/// To ensure the necessary data is available to cgi.d.\n\tvoid requireBasicAuth(string user, string pass, string message = null) {\n\t\tif(authorization != \"Basic \" ~ Base64.encode(cast(immutable(ubyte)[]) (user ~ \":\" ~ pass))) {\n\t\t\tsetResponseStatus(\"401 Authorization Required\");\n\t\t\theader (\"WWW-Authenticate: Basic realm=\\\"\"~message~\"\\\"\");\n\t\t\tclose();\n\t\t\tthrow new Exception(\"Not authorized; got \" ~ authorization);\n\t\t}\n\t}\n\n\t/// Very simple caching controls - setCache(false) means it will never be cached. Good for rapidly updated or sensitive sites.\n\t/// setCache(true) means it will always be cached for as long as possible. Best for static content.\n\t/// Use setResponseExpires and updateResponseExpires for more control\n\tvoid setCache(bool allowCaching) {\n\t\tnoCache = !allowCaching;\n\t}\n\n\t/// Set to true and use cgi.write(data, true); to send a gzipped response to browsers\n\t/// who can accept it\n\tbool gzipResponse;\n\n\timmutable bool acceptsGzip;\n\timmutable bool keepAliveRequested;\n\n\t/// Set to true if and only if this was initialized with command line arguments\n\timmutable bool isCalledWithCommandLineArguments;\n\n\t/// This gets a full url for the current request, including port, protocol, host, path, and query\n\tstring getCurrentCompleteUri() const {\n\t\tushort defaultPort = https ? 443 : 80;\n\n\t\tstring uri = \"http\";\n\t\tif(https)\n\t\t\turi ~= \"s\";\n\t\turi ~= \"://\";\n\t\turi ~= host;\n\t\t/+ // the host has the port so p sure this never needed, cgi on apache and embedded http all do the right thing now\n\t\tversion(none)\n\t\tif(!(!port || port == defaultPort)) {\n\t\t\turi ~= \":\";\n\t\t\turi ~= to!string(port);\n\t\t}\n\t\t+/\n\t\turi ~= requestUri;\n\t\treturn uri;\n\t}\n\n\t/// You can override this if your site base url isn't the same as the script name\n\tstring logicalScriptName() const {\n\t\treturn scriptName;\n\t}\n\n\t/++\n\t\tSets the HTTP status of the response. For example, \"404 File Not Found\" or \"500 Internal Server Error\".\n\t\tIt assumes \"200 OK\", and automatically changes to \"302 Found\" if you call setResponseLocation().\n\t\tNote setResponseStatus() must be called *before* you write() any data to the output.\n\n\t\tHistory:\n\t\t\tThe `int` overload was added on January 11, 2021.\n\t+/\n\tvoid setResponseStatus(string status) {\n\t\tassert(!outputtedResponseData);\n\t\tresponseStatus = status;\n\t}\n\t/// ditto\n\tvoid setResponseStatus(int statusCode) {\n\t\tsetResponseStatus(getHttpCodeText(statusCode));\n\t}\n\tprivate string responseStatus = null;\n\n\t/// Returns true if it is still possible to output headers\n\tbool canOutputHeaders() {\n\t\treturn !isClosed && !outputtedResponseData;\n\t}\n\n\t/// Sets the location header, which the browser will redirect the user to automatically.\n\t/// Note setResponseLocation() must be called *before* you write() any data to the output.\n\t/// The optional important argument is used if it's a default suggestion rather than something to insist upon.\n\tvoid setResponseLocation(string uri, bool important = true, string status = null) {\n\t\tif(!important && isCurrentResponseLocationImportant)\n\t\t\treturn; // important redirects always override unimportant ones\n\n\t\tif(uri is null) {\n\t\t\tresponseStatus = \"200 OK\";\n\t\t\tresponseLocation = null;\n\t\t\tisCurrentResponseLocationImportant = important;\n\t\t\treturn; // this just cancels the redirect\n\t\t}\n\n\t\tassert(!outputtedResponseData);\n\t\tif(status is null)\n\t\t\tresponseStatus = \"302 Found\";\n\t\telse\n\t\t\tresponseStatus = status;\n\n\t\tresponseLocation = uri.strip;\n\t\tisCurrentResponseLocationImportant = important;\n\t}\n\tprotected string responseLocation = null;\n\tprivate bool isCurrentResponseLocationImportant = false;\n\n\t/// Sets the Expires: http header. See also: updateResponseExpires, setPublicCaching\n\t/// The parameter is in unix_timestamp * 1000. Try setResponseExpires(getUTCtime() + SOME AMOUNT) for normal use.\n\t/// Note: the when parameter is different than setCookie's expire parameter.\n\tvoid setResponseExpires(long when, bool isPublic = false) {\n\t\tresponseExpires = when;\n\t\tsetCache(true); // need to enable caching so the date has meaning\n\n\t\tresponseIsPublic = isPublic;\n\t\tresponseExpiresRelative = false;\n\t}\n\n\t/// Sets a cache-control max-age header for whenFromNow, in seconds.\n\tvoid setResponseExpiresRelative(int whenFromNow, bool isPublic = false) {\n\t\tresponseExpires = whenFromNow;\n\t\tsetCache(true); // need to enable caching so the date has meaning\n\n\t\tresponseIsPublic = isPublic;\n\t\tresponseExpiresRelative = true;\n\t}\n\tprivate long responseExpires = long.min;\n\tprivate bool responseIsPublic = false;\n\tprivate bool responseExpiresRelative = false;\n\n\t/// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept.\n\t/// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program\n\t/// output as a whole is as cacheable as the least cacheable part in the chain.\n\n\t/// 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.\n\t/// Conversely, setting here overrides setCache(true), since any expiration date is in the past of infinity.\n\tvoid updateResponseExpires(long when, bool isPublic) {\n\t\tif(responseExpires == long.min)\n\t\t\tsetResponseExpires(when, isPublic);\n\t\telse if(when < responseExpires)\n\t\t\tsetResponseExpires(when, responseIsPublic && isPublic); // if any part of it is private, it all is\n\t}\n\n\t/*\n\t/// Set to true if you want the result to be cached publicly - that is, is the content shared?\n\t/// Should generally be false if the user is logged in. It assumes private cache only.\n\t/// setCache(true) also turns on public caching, and setCache(false) sets to private.\n\tvoid setPublicCaching(bool allowPublicCaches) {\n\t\tpublicCaching = allowPublicCaches;\n\t}\n\tprivate bool publicCaching = false;\n\t*/\n\n\t/++\n\t\tHistory:\n\t\t\tAdded January 11, 2021\n\t+/\n\tenum SameSitePolicy {\n\t\tLax,\n\t\tStrict,\n\t\tNone\n\t}\n\n\t/++\n\t\tSets an HTTP cookie, automatically encoding the data to the correct string.\n\t\texpiresIn is how many milliseconds in the future the cookie will expire.\n\t\tTIP: to make a cookie accessible from subdomains, set the domain to .yourdomain.com.\n\t\tNote setCookie() must be called *before* you write() any data to the output.\n\n\t\tHistory:\n\t\t\tParameter `sameSitePolicy` was added on January 11, 2021.\n\t+/\n\tvoid setCookie(string name, string data, long expiresIn = 0, string path = null, string domain = null, bool httpOnly = false, bool secure = false, SameSitePolicy sameSitePolicy = SameSitePolicy.Lax) {\n\t\tassert(!outputtedResponseData);\n\t\tstring cookie = std.uri.encodeComponent(name) ~ \"=\";\n\t\tcookie ~= std.uri.encodeComponent(data);\n\t\tif(path !is null)\n\t\t\tcookie ~= \"; path=\" ~ path;\n\t\t// FIXME: should I just be using max-age here? (also in cache below)\n\t\tif(expiresIn != 0)\n\t\t\tcookie ~= \"; expires=\" ~ printDate(cast(DateTime) Clock.currTime(UTC()) + dur!\"msecs\"(expiresIn));\n\t\tif(domain !is null)\n\t\t\tcookie ~= \"; domain=\" ~ domain;\n\t\tif(secure == true)\n\t\t\tcookie ~= \"; Secure\";\n\t\tif(httpOnly == true )\n\t\t\tcookie ~= \"; HttpOnly\";\n\t\tfinal switch(sameSitePolicy) {\n\t\t\tcase SameSitePolicy.Lax:\n\t\t\t\tcookie ~= \"; SameSite=Lax\";\n\t\t\tbreak;\n\t\t\tcase SameSitePolicy.Strict:\n\t\t\t\tcookie ~= \"; SameSite=Strict\";\n\t\t\tbreak;\n\t\t\tcase SameSitePolicy.None:\n\t\t\t\tcookie ~= \"; SameSite=None\";\n\t\t\t\tassert(secure); // cookie spec requires this now, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite\n\t\t\tbreak;\n\t\t}\n\n\t\tif(auto idx = name in cookieIndexes) {\n\t\t\tresponseCookies[*idx] = cookie;\n\t\t} else {\n\t\t\tcookieIndexes[name] = responseCookies.length;\n\t\t\tresponseCookies ~= cookie;\n\t\t}\n\t}\n\tprivate string[] responseCookies;\n\tprivate size_t[string] cookieIndexes;\n\n\t/// Clears a previously set cookie with the given name, path, and domain.\n\tvoid clearCookie(string name, string path = null, string domain = null) {\n\t\tassert(!outputtedResponseData);\n\t\tsetCookie(name, \"\", 1, path, domain);\n\t}\n\n\t/// Sets the content type of the response, for example \"text/html\" (the default) for HTML, or \"image/png\" for a PNG image\n\tvoid setResponseContentType(string ct) {\n\t\tassert(!outputtedResponseData);\n\t\tresponseContentType = ct;\n\t}\n\tprivate string responseContentType = null;\n\n\t/// Adds a custom header. It should be the name: value, but without any line terminator.\n\t/// For example: header(\"X-My-Header: Some value\");\n\t/// Note you should use the specialized functions in this object if possible to avoid\n\t/// duplicates in the output.\n\tvoid header(string h) {\n\t\tcustomHeaders ~= h;\n\t}\n\n\t/++\n\t\tI named the original function `header` after PHP, but this pattern more fits\n\t\tthe rest of the Cgi object.\n\n\t\tEither name are allowed.\n\n\t\tHistory:\n\t\t\tAlias added June 17, 2022.\n\t+/\n\talias setResponseHeader = header;\n\n\tprivate string[] customHeaders;\n\tprivate bool websocketMode;\n\n\tvoid flushHeaders(const(void)[] t, bool isAll = false) {\n\t\tStackBuffer buffer = StackBuffer(0);\n\n\t\tprepHeaders(t, isAll, &buffer);\n\n\t\tif(rawDataOutput !is null)\n\t\t\trawDataOutput(cast(const(ubyte)[]) buffer.get());\n\t\telse {\n\t\t\tstdout.rawWrite(buffer.get());\n\t\t}\n\t}\n\n\tprivate void prepHeaders(const(void)[] t, bool isAll, StackBuffer* buffer) {\n\t\tstring terminator = \"\\n\";\n\t\tif(rawDataOutput !is null)\n\t\t\tterminator = \"\\r\\n\";\n\n\t\tif(responseStatus !is null) {\n\t\t\tif(nph) {\n\t\t\t\tif(http10)\n\t\t\t\t\tbuffer.add(\"HTTP/1.0 \", responseStatus, terminator);\n\t\t\t\telse\n\t\t\t\t\tbuffer.add(\"HTTP/1.1 \", responseStatus, terminator);\n\t\t\t} else\n\t\t\t\tbuffer.add(\"Status: \", responseStatus, terminator);\n\t\t} else if (nph) {\n\t\t\tif(http10)\n\t\t\t\tbuffer.add(\"HTTP/1.0 200 OK\", terminator);\n\t\t\telse\n\t\t\t\tbuffer.add(\"HTTP/1.1 200 OK\", terminator);\n\t\t}\n\n\t\tif(websocketMode)\n\t\t\tgoto websocket;\n\n\t\tif(nph) { // we're responsible for setting the date too according to http 1.1\n\t\t\tchar[29] db = void;\n\t\t\tprintDateToBuffer(cast(DateTime) Clock.currTime(UTC()), db[]);\n\t\t\tbuffer.add(\"Date: \", db[], terminator);\n\t\t}\n\n\t\t// FIXME: what if the user wants to set his own content-length?\n\t\t// The custom header function can do it, so maybe that's best.\n\t\t// Or we could reuse the isAll param.\n\t\tif(responseLocation !is null) {\n\t\t\tbuffer.add(\"Location: \", responseLocation, terminator);\n\t\t}\n\t\tif(!noCache && responseExpires != long.min) { // an explicit expiration date is set\n\t\t\tif(responseExpiresRelative) {\n\t\t\t\tbuffer.add(\"Cache-Control: \", responseIsPublic ? \"public\" : \"private\", \", max-age=\");\n\t\t\t\tbuffer.add(responseExpires);\n\t\t\t\tbuffer.add(\", no-cache=\\\"set-cookie, set-cookie2\\\"\", terminator);\n\t\t\t} else {\n\t\t\t\tauto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC());\n\t\t\t\tchar[29] db = void;\n\t\t\t\tprintDateToBuffer(cast(DateTime) expires, db[]);\n\t\t\t\tbuffer.add(\"Expires: \", db[], terminator);\n\t\t\t\t// FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily\n\t\t\t\tbuffer.add(\"Cache-Control: \", (responseIsPublic ? \"public\" : \"private\"), \", no-cache=\\\"set-cookie, set-cookie2\\\"\");\n\t\t\t\tbuffer.add(terminator);\n\t\t\t}\n\t\t}\n\t\tif(responseCookies !is null && responseCookies.length > 0) {\n\t\t\tforeach(c; responseCookies)\n\t\t\t\tbuffer.add(\"Set-Cookie: \", c, terminator);\n\t\t}\n\t\tif(noCache) { // we specifically do not want caching (this is actually the default)\n\t\t\tbuffer.add(\"Cache-Control: private, no-cache=\\\"set-cookie\\\"\", terminator);\n\t\t\tbuffer.add(\"Expires: 0\", terminator);\n\t\t\tbuffer.add(\"Pragma: no-cache\", terminator);\n\t\t} else {\n\t\t\tif(responseExpires == long.min) { // caching was enabled, but without a date set - that means assume cache forever\n\t\t\t\tbuffer.add(\"Cache-Control: public\", terminator);\n\t\t\t\tbuffer.add(\"Expires: Tue, 31 Dec 2030 14:00:00 GMT\", terminator); // FIXME: should not be more than one year in the future\n\t\t\t}\n\t\t}\n\t\tif(responseContentType !is null) {\n\t\t\tbuffer.add(\"Content-Type: \", responseContentType, terminator);\n\t\t} else\n\t\t\tbuffer.add(\"Content-Type: text/html; charset=utf-8\", terminator);\n\n\t\tif(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary\n\t\t\tbuffer.add(\"Content-Encoding: gzip\", terminator);\n\t\t}\n\n\n\t\tif(!isAll) {\n\t\t\tif(nph && !http10) {\n\t\t\t\tbuffer.add(\"Transfer-Encoding: chunked\", terminator);\n\t\t\t\tresponseChunked = true;\n\t\t\t}\n\t\t} else {\n\t\t\tbuffer.add(\"Content-Length: \");\n\t\t\tbuffer.add(t.length);\n\t\t\tbuffer.add(terminator);\n\t\t\tif(nph && keepAliveRequested) {\n\t\t\t\tbuffer.add(\"Connection: Keep-Alive\", terminator);\n\t\t\t}\n\t\t}\n\n\t\twebsocket:\n\n\t\tforeach(hd; customHeaders)\n\t\t\tbuffer.add(hd, terminator);\n\n\t\t// FIXME: what about duplicated headers?\n\n\t\t// end of header indicator\n\t\tbuffer.add(terminator);\n\n\t\toutputtedResponseData = true;\n\t}\n\n\t/// Writes the data to the output, flushing headers if they have not yet been sent.\n\tvoid write(const(void)[] t, bool isAll = false, bool maybeAutoClose = true) {\n\t\tassert(!closed, \"Output has already been closed\");\n\n\t\tStackBuffer buffer = StackBuffer(0);\n\n\t\tif(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary\n\t\t\t// actually gzip the data here\n\n\t\t\tauto c = new Compress(HeaderFormat.gzip); // want gzip\n\n\t\t\tauto data = c.compress(t);\n\t\t\tdata ~= c.flush();\n\n\t\t\t// std.file.write(\"/tmp/last-item\", data);\n\n\t\t\tt = data;\n\t\t}\n\n\t\tif(!outputtedResponseData && (!autoBuffer || isAll)) {\n\t\t\tprepHeaders(t, isAll, &buffer);\n\t\t}\n\n\t\tif(requestMethod != RequestMethod.HEAD && t.length > 0) {\n\t\t\tif (autoBuffer && !isAll) {\n\t\t\t\toutputBuffer ~= cast(ubyte[]) t;\n\t\t\t}\n\t\t\tif(!autoBuffer || isAll) {\n\t\t\t\tif(rawDataOutput !is null)\n\t\t\t\t\tif(nph && responseChunked) {\n\t\t\t\t\t\t//rawDataOutput(makeChunk(cast(const(ubyte)[]) t));\n\t\t\t\t\t\t// we're making the chunk here instead of in a function\n\t\t\t\t\t\t// to avoid unneeded gc pressure\n\t\t\t\t\t\tbuffer.add(toHex(t.length));\n\t\t\t\t\t\tbuffer.add(\"\\r\\n\");\n\t\t\t\t\t\tbuffer.add(cast(char[]) t, \"\\r\\n\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbuffer.add(cast(char[]) t);\n\t\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\tbuffer.add(cast(char[]) t);\n\t\t\t}\n\t\t}\n\n\t\tif(rawDataOutput !is null)\n\t\t\trawDataOutput(cast(const(ubyte)[]) buffer.get());\n\t\telse\n\t\t\tstdout.rawWrite(buffer.get());\n\n\t\tif(maybeAutoClose && isAll)\n\t\t\tclose(); // if you say it is all, that means we're definitely done\n\t\t\t\t// maybeAutoClose can be false though to avoid this (important if you call from inside close()!\n\t}\n\n\t/++\n\t\tConvenience method to set content type to json and write the string as the complete response.\n\n\t\tHistory:\n\t\t\tAdded January 16, 2020\n\t+/\n\tvoid writeJson(string json) {\n\t\tthis.setResponseContentType(\"application/json\");\n\t\tthis.write(json, true);\n\t}\n\n\t/// Flushes the pending buffer, leaving the connection open so you can send more.\n\tvoid flush() {\n\t\tif(rawDataOutput is null)\n\t\t\tstdout.flush();\n\t\telse if(flushDelegate !is null)\n\t\t\tflushDelegate();\n\t}\n\n\tversion(autoBuffer)\n\t\tbool autoBuffer = true;\n\telse\n\t\tbool autoBuffer = false;\n\tubyte[] outputBuffer;\n\n\t/// Flushes the buffers to the network, signifying that you are done.\n\t/// You should always call this explicitly when you are done outputting data.\n\tvoid close() {\n\t\tif(closed)\n\t\t\treturn; // don't double close\n\n\t\tif(!outputtedResponseData)\n\t\t\twrite(\"\", true, false);\n\n\t\t// writing auto buffered data\n\t\tif(requestMethod != RequestMethod.HEAD && autoBuffer) {\n\t\t\tif(!nph)\n\t\t\t\tstdout.rawWrite(outputBuffer);\n\t\t\telse\n\t\t\t\twrite(outputBuffer, true, false); // tell it this is everything\n\t\t}\n\n\t\t// closing the last chunk...\n\t\tif(nph && rawDataOutput !is null && responseChunked)\n\t\t\trawDataOutput(cast(const(ubyte)[]) \"0\\r\\n\\r\\n\");\n\n\t\tif(flushDelegate)\n\t\t\tflushDelegate();\n\n\t\tclosed = true;\n\t}\n\n\t// Closes without doing anything, shouldn't be used often\n\tvoid rawClose() {\n\t\tclosed = true;\n\t}\n\n\t/++\n\t\tGets a request variable as a specific type, or the default value of it isn't there\n\t\tor isn't convertible to the request type.\n\n\t\tChecks both GET and POST variables, preferring the POST variable, if available.\n\n\t\tA nice trick is using the default value to choose the type:\n\n\t\t---\n\t\t\t/*\n\t\t\t\tThe return value will match the type of the default.\n\t\t\t\tHere, I gave 10 as a default, so the return value will\n\t\t\t\tbe an int.\n\n\t\t\t\tIf the user-supplied value cannot be converted to the\n\t\t\t\trequested type, you will get the default value back.\n\t\t\t*/\n\t\t\tint a = cgi.request(\"number\", 10);\n\n\t\t\tif(cgi.get[\"number\"] == \"11\")\n\t\t\t\tassert(a == 11); // conversion succeeds\n\n\t\t\tif(\"number\" !in cgi.get)\n\t\t\t\tassert(a == 10); // no value means you can't convert - give the default\n\n\t\t\tif(cgi.get[\"number\"] == \"twelve\")\n\t\t\t\tassert(a == 10); // conversion from string to int would fail, so we get the default\n\t\t---\n\n\t\tYou can use an enum as an easy whitelist, too:\n\n\t\t---\n\t\t\tenum Operations {\n\t\t\t\tadd, remove, query\n\t\t\t}\n\n\t\t\tauto op = cgi.request(\"op\", Operations.query);\n\n\t\t\tif(cgi.get[\"op\"] == \"add\")\n\t\t\t\tassert(op == Operations.add);\n\t\t\tif(cgi.get[\"op\"] == \"remove\")\n\t\t\t\tassert(op == Operations.remove);\n\t\t\tif(cgi.get[\"op\"] == \"query\")\n\t\t\t\tassert(op == Operations.query);\n\n\t\t\tif(cgi.get[\"op\"] == \"random string\")\n\t\t\t\tassert(op == Operations.query); // the value can't be converted to the enum, so we get the default\n\t\t---\n\t+/\n\tT request(T = string)(in string name, in T def = T.init) const nothrow {\n\t\ttry {\n\t\t\treturn\n\t\t\t\t(name in post) ? to!T(post[name]) :\n\t\t\t\t(name in get)  ? to!T(get[name]) :\n\t\t\t\tdef;\n\t\t} catch(Exception e) { return def; }\n\t}\n\n\t/// Is the output already closed?\n\tbool isClosed() const {\n\t\treturn closed;\n\t}\n\n\t/++\n\t\tGets a session object associated with the `cgi` request. You can use different type throughout your application.\n\t+/\n\tSession!Data getSessionObject(Data)() {\n\t\tif(testInProcess !is null) {\n\t\t\t// test mode\n\t\t\tauto obj = testInProcess.getSessionOverride(typeid(typeof(return)));\n\t\t\tif(obj !is null)\n\t\t\t\treturn cast(typeof(return)) obj;\n\t\t\telse {\n\t\t\t\tauto o = new MockSession!Data();\n\t\t\t\ttestInProcess.setSessionOverride(typeid(typeof(return)), o);\n\t\t\t\treturn o;\n\t\t\t}\n\t\t} else {\n\t\t\t// normal operation\n\t\t\treturn new BasicDataServerSession!Data(this);\n\t\t}\n\t}\n\n\t// if it is in test mode; triggers mock sessions. Used by CgiTester\n\tversion(with_breaking_cgi_features)\n\tprivate CgiTester testInProcess;\n\n\t/* Hooks for redirecting input and output */\n\tprivate void delegate(const(ubyte)[]) rawDataOutput = null;\n\tprivate void delegate() flushDelegate = null;\n\n\t/* This info is used when handling a more raw HTTP protocol */\n\tprivate bool nph;\n\tprivate bool http10;\n\tprivate bool closed;\n\tprivate bool responseChunked = false;\n\n\tversion(preserveData) // note: this can eat lots of memory; don't use unless you're sure you need it.\n\timmutable(ubyte)[] originalPostData;\n\n\t/++\n\t\tThis holds the posted body data if it has not been parsed into [post] and [postArray].\n\n\t\tIt is intended to be used for JSON and XML request content types, but also may be used\n\t\tfor other content types your application can handle. But it will NOT be populated\n\t\tfor content types application/x-www-form-urlencoded or multipart/form-data, since those are\n\t\tparsed into the post and postArray members.\n\n\t\tRemember that anything beyond your `maxContentLength` param when setting up [GenericMain], etc.,\n\t\twill be discarded to the client with an error. This helps keep this array from being exploded in size\n\t\tand consuming all your server's memory (though it may still be possible to eat excess ram from a concurrent\n\t\tclient in certain build modes.)\n\n\t\tHistory:\n\t\t\tAdded January 5, 2021\n\t\t\tDocumented February 21, 2023 (dub v11.0)\n\t+/\n\tpublic immutable string postBody;\n\talias postJson = postBody; // old name\n\n\t/* Internal state flags */\n\tprivate bool outputtedResponseData;\n\tprivate bool noCache = true;\n\n\tconst(string[string]) environmentVariables;\n\n\t/** What follows is data gotten from the HTTP request. It is all fully immutable,\n\t    partially because it logically is (your code doesn't change what the user requested...)\n\t    and partially because I hate how bad programs in PHP change those superglobals to do\n\t    all kinds of hard to follow ugliness. I don't want that to ever happen in D.\n\n\t    For some of these, you'll want to refer to the http or cgi specs for more details.\n\t*/\n\timmutable(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.\n\n\timmutable(char[]) host; \t/// The hostname in the request. If one program serves multiple domains, you can use this to differentiate between them.\n\timmutable(char[]) origin; \t/// 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.\n\timmutable(char[]) userAgent; \t/// The browser's user-agent string. Can be used to identify the browser.\n\timmutable(char[]) pathInfo; \t/// 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\".\n\timmutable(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\".\n\timmutable(char[]) scriptFileName;   /// The physical filename of your script\n\timmutable(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.\n\timmutable(char[]) accept; \t/// 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.)\n\timmutable(char[]) lastEventId; \t/// 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.\n\n\timmutable(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.\n\timmutable(char[]) queryString; \t/// 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.)\n\timmutable(char[]) cookie; \t/// The unparsed content of the Cookie: header in the request. See also the cookies[string] member for a parsed view of the data.\n\t/** 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.\n\n\tImportant 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.\n\t*/\n\timmutable(char[]) referrer;\n\timmutable(char[]) requestUri; \t/// The full url if the current request, excluding the protocol and host. requestUri == scriptName ~ pathInfo ~ (queryString.length ? \"?\" ~ queryString : \"\");\n\n\timmutable(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.)\n\n\timmutable bool https; \t/// Was the request encrypted via https?\n\timmutable int port; \t/// On what TCP port number did the server receive the request?\n\n\t/** Here come the parsed request variables - the things that come close to PHP's _GET, _POST, etc. superglobals in content. */\n\n\timmutable(string[string]) get; \t/// 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.\n\timmutable(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.\n\timmutable(string[string]) cookies; /// Separates out the cookie header into individual name/value pairs (which is how you set them!)\n\n\t/**\n\t\tRepresents user uploaded files.\n\n\t\tWhen making a file upload form, be sure to follow the standard: set method=\"POST\" and enctype=\"multipart/form-data\" in your html <form> 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.\n\t*/\n\timmutable(UploadedFile[][string]) filesArray;\n\timmutable(UploadedFile[string]) files;\n\n\t/// 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.\n\t/// the order of the arrays is the order the data arrives\n\timmutable(string[][string]) getArray; /// like get, but an array of values per name\n\timmutable(string[][string]) postArray; /// ditto for post\n\timmutable(string[][string]) cookiesArray; /// ditto for cookies\n\n\t// convenience function for appending to a uri without extra ?\n\t// matches the name and effect of javascript's location.search property\n\tstring search() const {\n\t\tif(queryString.length)\n\t\t\treturn \"?\" ~ queryString;\n\t\treturn \"\";\n\t}\n\n\t// FIXME: what about multiple files with the same name?\n  private:\n\t//RequestMethod _requestMethod;\n}\n\n/// use this for testing or other isolated things when you want it to be no-ops\nCgi dummyCgi(Cgi.RequestMethod method = Cgi.RequestMethod.GET, string url = null, in ubyte[] data = null, void delegate(const(ubyte)[]) outputSink = null) {\n\t// we want to ignore, not use stdout\n\tif(outputSink is null)\n\t\toutputSink = delegate void(const(ubyte)[]) { };\n\n\tstring[string] env;\n\tenv[\"REQUEST_METHOD\"] = to!string(method);\n\tenv[\"CONTENT_LENGTH\"] = to!string(data.length);\n\n\tauto cgi = new Cgi(\n\t\t0,\n\t\tenv,\n\t\t{ return data; },\n\t\toutputSink,\n\t\tnull);\n\n\treturn cgi;\n}\n\n/++\n\tA helper test class for request handler unittests.\n+/\nversion(with_breaking_cgi_features)\nclass CgiTester {\n\tprivate {\n\t\tSessionObject[TypeInfo] mockSessions;\n\t\tSessionObject getSessionOverride(TypeInfo ti) {\n\t\t\tif(auto o = ti in mockSessions)\n\t\t\t\treturn *o;\n\t\t\telse\n\t\t\t\treturn null;\n\t\t}\n\t\tvoid setSessionOverride(TypeInfo ti, SessionObject so) {\n\t\t\tmockSessions[ti] = so;\n\t\t}\n\t}\n\n\t/++\n\t\tGets (and creates if necessary) a mock session object for this test. Note\n\t\tit will be the same one used for any test operations through this CgiTester instance.\n\t+/\n\tSession!Data getSessionObject(Data)() {\n\t\tauto obj = getSessionOverride(typeid(typeof(return)));\n\t\tif(obj !is null)\n\t\t\treturn cast(typeof(return)) obj;\n\t\telse {\n\t\t\tauto o = new MockSession!Data();\n\t\t\tsetSessionOverride(typeid(typeof(return)), o);\n\t\t\treturn o;\n\t\t}\n\t}\n\n\t/++\n\t\tPass a reference to your request handler when creating the tester.\n\t+/\n\tthis(void function(Cgi) requestHandler) {\n\t\tthis.requestHandler = requestHandler;\n\t}\n\n\t/++\n\t\tYou can check response information with these methods after you call the request handler.\n\t+/\n\tstruct Response {\n\t\tint code;\n\t\tstring[string] headers;\n\t\tstring responseText;\n\t\tubyte[] responseBody;\n\t}\n\n\t/++\n\t\tExecutes a test request on your request handler, and returns the response.\n\n\t\tParams:\n\t\t\turl = The URL to test. Should be an absolute path, but excluding domain. e.g. `\"/test\"`.\n\t\t\targs = additional arguments. Same format as cgi's command line handler.\n\t+/\n\tResponse GET(string url, string[] args = null) {\n\t\treturn executeTest(\"GET\", url, args);\n\t}\n\t/// ditto\n\tResponse POST(string url, string[] args = null) {\n\t\treturn executeTest(\"POST\", url, args);\n\t}\n\n\t/// ditto\n\tResponse executeTest(string method, string url, string[] args) {\n\t\tubyte[] outputtedRawData;\n\t\tvoid outputSink(const(ubyte)[] data) {\n\t\t\toutputtedRawData ~= data;\n\t\t}\n\t\tauto cgi = new Cgi([\"test\", method, url] ~ args, &outputSink);\n\t\tcgi.testInProcess = this;\n\t\tscope(exit) cgi.dispose();\n\n\t\trequestHandler(cgi);\n\n\t\tcgi.close();\n\n\t\tResponse response;\n\n\t\tif(outputtedRawData.length) {\n\t\t\tenum LINE = \"\\r\\n\";\n\n\t\t\tauto idx = outputtedRawData.locationOf(LINE ~ LINE);\n\t\t\tassert(idx != -1, to!string(outputtedRawData));\n\t\t\tauto headers = cast(string) outputtedRawData[0 .. idx];\n\t\t\tresponse.code = 200;\n\t\t\twhile(headers.length) {\n\t\t\t\tauto i = headers.locationOf(LINE);\n\t\t\t\tif(i == -1) i = cast(int) headers.length;\n\n\t\t\t\tauto header = headers[0 .. i];\n\n\t\t\t\tauto c = header.locationOf(\":\");\n\t\t\t\tif(c != -1) {\n\t\t\t\t\tauto name = header[0 .. c];\n\t\t\t\t\tauto value = header[c + 2 ..$];\n\n\t\t\t\t\tif(name == \"Status\")\n\t\t\t\t\t\tresponse.code = value[0 .. value.locationOf(\" \")].to!int;\n\n\t\t\t\t\tresponse.headers[name] = value;\n\t\t\t\t} else {\n\t\t\t\t\tassert(0);\n\t\t\t\t}\n\n\t\t\t\tif(i != headers.length)\n\t\t\t\t\ti += 2;\n\t\t\t\theaders = headers[i .. $];\n\t\t\t}\n\t\t\tresponse.responseBody = outputtedRawData[idx + 4 .. $];\n\t\t\tresponse.responseText = cast(string) response.responseBody;\n\t\t}\n\n\t\treturn response;\n\t}\n\n\tprivate void function(Cgi) requestHandler;\n}\n\n\n// should this be a separate module? Probably, but that's a hassle.\n\n/// Makes a data:// uri that can be used as links in most newer browsers (IE8+).\nstring makeDataUrl(string mimeType, in void[] data) {\n\tauto data64 = Base64.encode(cast(const(ubyte[])) data);\n\treturn \"data:\" ~ mimeType ~ \";base64,\" ~ assumeUnique(data64);\n}\n\n// FIXME: I don't think this class correctly decodes/encodes the individual parts\n/// Represents a url that can be broken down or built up through properties\nstruct Uri {\n\talias toString this; // blargh idk a url really is a string, but should it be implicit?\n\n\t// scheme//userinfo@host:port/path?query#fragment\n\n\tstring scheme; /// e.g. \"http\" in \"http://example.com/\"\n\tstring userinfo; /// the username (and possibly a password) in the uri\n\tstring host; /// the domain name\n\tint port; /// port number, if given. Will be zero if a port was not explicitly given\n\tstring path; /// e.g. \"/folder/file.html\" in \"http://example.com/folder/file.html\"\n\tstring query; /// the stuff after the ? in a uri\n\tstring fragment; /// the stuff after the # in a uri.\n\n\t// 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\n\t// the decode ones need to keep different names anyway because we can't overload on return values...\n\tstatic string encode(string s) { return std.uri.encodeComponent(s); }\n\tstatic string encode(string[string] s) { return encodeVariables(s); }\n\tstatic string encode(string[][string] s) { return encodeVariables(s); }\n\n\t/// Breaks down a uri string to its components\n\tthis(string uri) {\n\t\treparse(uri);\n\t}\n\n\tprivate void reparse(string uri) {\n\t\t// from RFC 3986\n\t\t// the ctRegex triples the compile time and makes ugly errors for no real benefit\n\t\t// it was a nice experiment but just not worth it.\n\t\t// enum ctr = ctRegex!r\"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?\";\n\t\t/*\n\t\t\tCaptures:\n\t\t\t\t0 = whole url\n\t\t\t\t1 = scheme, with :\n\t\t\t\t2 = scheme, no :\n\t\t\t\t3 = authority, with //\n\t\t\t\t4 = authority, no //\n\t\t\t\t5 = path\n\t\t\t\t6 = query string, with ?\n\t\t\t\t7 = query string, no ?\n\t\t\t\t8 = anchor, with #\n\t\t\t\t9 = anchor, no #\n\t\t*/\n\t\t// Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer!\n\t\t// instead, I will DIY and cut that down to 0.6s on the same computer.\n\t\t/*\n\n\t\t\t\tNote that authority is\n\t\t\t\t\tuser:password@domain:port\n\t\t\t\twhere the user:password@ part is optional, and the :port is optional.\n\n\t\t\t\tRegex translation:\n\n\t\t\t\tScheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first.\n\t\t\t\tAuthority must start with //, but cannot have any other /, ?, or # in it. It is optional.\n\t\t\t\tPath cannot have any ? or # in it. It is optional.\n\t\t\t\tQuery must start with ? and must not have # in it. It is optional.\n\t\t\t\tAnchor must start with # and can have anything else in it to end of string. It is optional.\n\t\t*/\n\n\t\tthis = Uri.init; // reset all state\n\n\t\t// empty uri = nothing special\n\t\tif(uri.length == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tsize_t idx;\n\n\t\tscheme_loop: foreach(char c; uri[idx .. $]) {\n\t\t\tswitch(c) {\n\t\t\t\tcase ':':\n\t\t\t\tcase '/':\n\t\t\t\tcase '?':\n\t\t\t\tcase '#':\n\t\t\t\t\tbreak scheme_loop;\n\t\t\t\tdefault:\n\t\t\t}\n\t\t\tidx++;\n\t\t}\n\n\t\tif(idx == 0 && uri[idx] == ':') {\n\t\t\t// this is actually a path! we skip way ahead\n\t\t\tgoto path_loop;\n\t\t}\n\n\t\tif(idx == uri.length) {\n\t\t\t// the whole thing is a path, apparently\n\t\t\tpath = uri;\n\t\t\treturn;\n\t\t}\n\n\t\tif(idx > 0 && uri[idx] == ':') {\n\t\t\tscheme = uri[0 .. idx];\n\t\t\tidx++;\n\t\t} else {\n\t\t\t// we need to rewind; it found a / but no :, so the whole thing is prolly a path...\n\t\t\tidx = 0;\n\t\t}\n\n\t\tif(idx + 2 < uri.length && uri[idx .. idx + 2] == \"//\") {\n\t\t\t// we have an authority....\n\t\t\tidx += 2;\n\n\t\t\tauto authority_start = idx;\n\t\t\tauthority_loop: foreach(char c; uri[idx .. $]) {\n\t\t\t\tswitch(c) {\n\t\t\t\t\tcase '/':\n\t\t\t\t\tcase '?':\n\t\t\t\t\tcase '#':\n\t\t\t\t\t\tbreak authority_loop;\n\t\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tidx++;\n\t\t\t}\n\n\t\t\tauto authority = uri[authority_start .. idx];\n\n\t\t\tauto idx2 = authority.indexOf(\"@\");\n\t\t\tif(idx2 != -1) {\n\t\t\t\tuserinfo = authority[0 .. idx2];\n\t\t\t\tauthority = authority[idx2 + 1 .. $];\n\t\t\t}\n\n\t\t\tif(authority.length && authority[0] == '[') {\n\t\t\t\t// ipv6 address special casing\n\t\t\t\tidx2 = authority.indexOf(']');\n\t\t\t\tif(idx2 != -1) {\n\t\t\t\t\tauto end = authority[idx2 + 1 .. $];\n\t\t\t\t\tif(end.length && end[0] == ':')\n\t\t\t\t\t\tidx2 = idx2 + 1;\n\t\t\t\t\telse\n\t\t\t\t\t\tidx2 = -1;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tidx2 = authority.indexOf(\":\");\n\t\t\t}\n\n\t\t\tif(idx2 == -1) {\n\t\t\t\tport = 0; // 0 means not specified; we should use the default for the scheme\n\t\t\t\thost = authority;\n\t\t\t} else {\n\t\t\t\thost = authority[0 .. idx2];\n\t\t\t\tif(idx2 + 1 < authority.length)\n\t\t\t\t\tport = to!int(authority[idx2 + 1 .. $]);\n\t\t\t\telse\n\t\t\t\t\tport = 0;\n\t\t\t}\n\t\t}\n\n\t\tpath_loop:\n\t\tauto path_start = idx;\n\n\t\tforeach(char c; uri[idx .. $]) {\n\t\t\tif(c == '?' || c == '#')\n\t\t\t\tbreak;\n\t\t\tidx++;\n\t\t}\n\n\t\tpath = uri[path_start .. idx];\n\n\t\tif(idx == uri.length)\n\t\t\treturn; // nothing more to examine...\n\n\t\tif(uri[idx] == '?') {\n\t\t\tidx++;\n\t\t\tauto query_start = idx;\n\t\t\tforeach(char c; uri[idx .. $]) {\n\t\t\t\tif(c == '#')\n\t\t\t\t\tbreak;\n\t\t\t\tidx++;\n\t\t\t}\n\t\t\tquery = uri[query_start .. idx];\n\t\t}\n\n\t\tif(idx < uri.length && uri[idx] == '#') {\n\t\t\tidx++;\n\t\t\tfragment = uri[idx .. $];\n\t\t}\n\n\t\t// uriInvalidated = false;\n\t}\n\n\tprivate string rebuildUri() const {\n\t\tstring ret;\n\t\tif(scheme.length)\n\t\t\tret ~= scheme ~ \":\";\n\t\tif(userinfo.length || host.length)\n\t\t\tret ~= \"//\";\n\t\tif(userinfo.length)\n\t\t\tret ~= userinfo ~ \"@\";\n\t\tif(host.length)\n\t\t\tret ~= host;\n\t\tif(port)\n\t\t\tret ~= \":\" ~ to!string(port);\n\n\t\tret ~= path;\n\n\t\tif(query.length)\n\t\t\tret ~= \"?\" ~ query;\n\n\t\tif(fragment.length)\n\t\t\tret ~= \"#\" ~ fragment;\n\n\t\t// uri = ret;\n\t\t// uriInvalidated = false;\n\t\treturn ret;\n\t}\n\n\t/// Converts the broken down parts back into a complete string\n\tstring toString() const {\n\t\t// if(uriInvalidated)\n\t\t\treturn rebuildUri();\n\t}\n\n\t/// Returns a new absolute Uri given a base. It treats this one as\n\t/// relative where possible, but absolute if not. (If protocol, domain, or\n\t/// other info is not set, the new one inherits it from the base.)\n\t///\n\t/// Browsers use a function like this to figure out links in html.\n\tUri basedOn(in Uri baseUrl) const {\n\t\tUri n = this; // copies\n\t\tif(n.scheme == \"data\")\n\t\t\treturn n;\n\t\t// n.uriInvalidated = true; // make sure we regenerate...\n\n\t\t// userinfo is not inherited... is this wrong?\n\n\t\t// if anything is given in the existing url, we don't use the base anymore.\n\t\tif(n.scheme.empty) {\n\t\t\tn.scheme = baseUrl.scheme;\n\t\t\tif(n.host.empty) {\n\t\t\t\tn.host = baseUrl.host;\n\t\t\t\tif(n.port == 0) {\n\t\t\t\t\tn.port = baseUrl.port;\n\t\t\t\t\tif(n.path.length > 0 && n.path[0] != '/') {\n\t\t\t\t\t\tauto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf(\"/\") + 1];\n\t\t\t\t\t\tif(b.length == 0)\n\t\t\t\t\t\t\tb = \"/\";\n\t\t\t\t\t\tn.path = b ~ n.path;\n\t\t\t\t\t} else if(n.path.length == 0) {\n\t\t\t\t\t\tn.path = baseUrl.path;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tn.removeDots();\n\n\t\treturn n;\n\t}\n\n\tvoid removeDots() {\n\t\tauto parts = this.path.split(\"/\");\n\t\tstring[] toKeep;\n\t\tforeach(part; parts) {\n\t\t\tif(part == \".\") {\n\t\t\t\tcontinue;\n\t\t\t} else if(part == \"..\") {\n\t\t\t\t//if(toKeep.length > 1)\n\t\t\t\t\ttoKeep = toKeep[0 .. $-1];\n\t\t\t\t//else\n\t\t\t\t\t//toKeep = [\"\"];\n\t\t\t\tcontinue;\n\t\t\t} else {\n\t\t\t\t//if(toKeep.length && toKeep[$-1].length == 0 && part.length == 0)\n\t\t\t\t\t//continue; // skip a `//` situation\n\t\t\t\ttoKeep ~= part;\n\t\t\t}\n\t\t}\n\n\t\tauto path = toKeep.join(\"/\");\n\t\tif(path.length && path[0] != '/')\n\t\t\tpath = \"/\" ~ path;\n\n\t\tthis.path = path;\n\t}\n\n\tunittest {\n\t\tauto uri = Uri(\"test.html\");\n\t\tassert(uri.path == \"test.html\");\n\t\turi = Uri(\"path/1/lol\");\n\t\tassert(uri.path == \"path/1/lol\");\n\t\turi = Uri(\"http://me@example.com\");\n\t\tassert(uri.scheme == \"http\");\n\t\tassert(uri.userinfo == \"me\");\n\t\tassert(uri.host == \"example.com\");\n\t\turi = Uri(\"http://example.com/#a\");\n\t\tassert(uri.scheme == \"http\");\n\t\tassert(uri.host == \"example.com\");\n\t\tassert(uri.fragment == \"a\");\n\t\turi = Uri(\"#foo\");\n\t\tassert(uri.fragment == \"foo\");\n\t\turi = Uri(\"?lol\");\n\t\tassert(uri.query == \"lol\");\n\t\turi = Uri(\"#foo?lol\");\n\t\tassert(uri.fragment == \"foo?lol\");\n\t\turi = Uri(\"?lol#foo\");\n\t\tassert(uri.fragment == \"foo\");\n\t\tassert(uri.query == \"lol\");\n\n\t\turi = Uri(\"http://127.0.0.1/\");\n\t\tassert(uri.host == \"127.0.0.1\");\n\t\tassert(uri.port == 0);\n\n\t\turi = Uri(\"http://127.0.0.1:123/\");\n\t\tassert(uri.host == \"127.0.0.1\");\n\t\tassert(uri.port == 123);\n\n\t\turi = Uri(\"http://[ff:ff::0]/\");\n\t\tassert(uri.host == \"[ff:ff::0]\");\n\n\t\turi = Uri(\"http://[ff:ff::0]:123/\");\n\t\tassert(uri.host == \"[ff:ff::0]\");\n\t\tassert(uri.port == 123);\n\t}\n\n\t// This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover\n\t// the possibilities.\n\tunittest {\n\t\tauto url = Uri(\"cool.html\"); // checking relative links\n\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html\")) == \"http://test.com/what/cool.html\");\n\t\tassert(url.basedOn(Uri(\"https://test.com/what/test.html\")) == \"https://test.com/what/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/\")) == \"http://test.com/what/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/\")) == \"http://test.com/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com\")) == \"http://test.com/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b\")) == \"http://test.com/what/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b&c=d\")) == \"http://test.com/what/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b&c=d#what\")) == \"http://test.com/what/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com\")) == \"http://test.com/cool.html\");\n\n\t\turl = Uri(\"/something/cool.html\"); // same server, different path\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html\")) == \"http://test.com/something/cool.html\");\n\t\tassert(url.basedOn(Uri(\"https://test.com/what/test.html\")) == \"https://test.com/something/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/\")) == \"http://test.com/something/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/\")) == \"http://test.com/something/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com\")) == \"http://test.com/something/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b\")) == \"http://test.com/something/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b&c=d\")) == \"http://test.com/something/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b&c=d#what\")) == \"http://test.com/something/cool.html\");\n\t\tassert(url.basedOn(Uri(\"http://test.com\")) == \"http://test.com/something/cool.html\");\n\n\t\turl = Uri(\"?query=answer\"); // same path. server, protocol, and port, just different query string and fragment\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html\")) == \"http://test.com/what/test.html?query=answer\");\n\t\tassert(url.basedOn(Uri(\"https://test.com/what/test.html\")) == \"https://test.com/what/test.html?query=answer\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/\")) == \"http://test.com/what/?query=answer\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/\")) == \"http://test.com/?query=answer\");\n\t\tassert(url.basedOn(Uri(\"http://test.com\")) == \"http://test.com?query=answer\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b\")) == \"http://test.com/what/test.html?query=answer\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b&c=d\")) == \"http://test.com/what/test.html?query=answer\");\n\t\tassert(url.basedOn(Uri(\"http://test.com/what/test.html?a=b&c=d#what\")) == \"http://test.com/what/test.html?query=answer\");\n\t\tassert(url.basedOn(Uri(\"http://test.com\")) == \"http://test.com?query=answer\");\n\n\t\turl = Uri(\"/test/bar\");\n\t\tassert(Uri(\"./\").basedOn(url) == \"/test/\", Uri(\"./\").basedOn(url));\n\t\tassert(Uri(\"../\").basedOn(url) == \"/\");\n\n\t\turl = Uri(\"http://example.com/\");\n\t\tassert(Uri(\"../foo\").basedOn(url) == \"http://example.com/foo\");\n\n\t\t//auto uriBefore = url;\n\t\turl = Uri(\"#anchor\"); // everything should remain the same except the anchor\n\t\t//uriBefore.anchor = \"anchor\");\n\t\t//assert(url == uriBefore);\n\n\t\turl = Uri(\"//example.com\"); // same protocol, but different server. the path here should be blank.\n\n\t\turl = Uri(\"//example.com/example.html\"); // same protocol, but different server and path\n\n\t\turl = Uri(\"http://example.com/test.html\"); // completely absolute link should never be modified\n\n\t\turl = Uri(\"http://example.com\"); // completely absolute link should never be modified, even if it has no path\n\n\t\t// FIXME: add something for port too\n\t}\n\n\t// these are like javascript's location.search and location.hash\n\tstring search() const {\n\t\treturn query.length ? (\"?\" ~ query) : \"\";\n\t}\n\tstring hash() const {\n\t\treturn fragment.length ? (\"#\" ~ fragment) : \"\";\n\t}\n}\n\n\n/*\n\tfor session, see web.d\n*/\n\n/// breaks down a url encoded string\nstring[][string] decodeVariables(string data, string separator = \"&\", string[]* namesInOrder = null, string[]* valuesInOrder = null) {\n\tauto vars = data.split(separator);\n\tstring[][string] _get;\n\tforeach(var; vars) {\n\t\tauto equal = var.indexOf(\"=\");\n\t\tstring name;\n\t\tstring value;\n\t\tif(equal == -1) {\n\t\t\tname = decodeComponent(var);\n\t\t\tvalue = \"\";\n\t\t} else {\n\t\t\t//_get[decodeComponent(var[0..equal])] ~= decodeComponent(var[equal + 1 .. $].replace(\"+\", \" \"));\n\t\t\t// stupid + -> space conversion.\n\t\t\tname = decodeComponent(var[0..equal].replace(\"+\", \" \"));\n\t\t\tvalue = decodeComponent(var[equal + 1 .. $].replace(\"+\", \" \"));\n\t\t}\n\n\t\t_get[name] ~= value;\n\t\tif(namesInOrder)\n\t\t\t(*namesInOrder) ~= name;\n\t\tif(valuesInOrder)\n\t\t\t(*valuesInOrder) ~= value;\n\t}\n\treturn _get;\n}\n\n/// breaks down a url encoded string, but only returns the last value of any array\nstring[string] decodeVariablesSingle(string data) {\n\tstring[string] va;\n\tauto varArray = decodeVariables(data);\n\tforeach(k, v; varArray)\n\t\tva[k] = v[$-1];\n\n\treturn va;\n}\n\n/// url encodes the whole string\nstring encodeVariables(in string[string] data) {\n\tstring ret;\n\n\tbool outputted = false;\n\tforeach(k, v; data) {\n\t\tif(outputted)\n\t\t\tret ~= \"&\";\n\t\telse\n\t\t\toutputted = true;\n\n\t\tret ~= std.uri.encodeComponent(k) ~ \"=\" ~ std.uri.encodeComponent(v);\n\t}\n\n\treturn ret;\n}\n\n/// url encodes a whole string\nstring encodeVariables(in string[][string] data) {\n\tstring ret;\n\n\tbool outputted = false;\n\tforeach(k, arr; data) {\n\t\tforeach(v; arr) {\n\t\t\tif(outputted)\n\t\t\t\tret ~= \"&\";\n\t\t\telse\n\t\t\t\toutputted = true;\n\t\t\tret ~= std.uri.encodeComponent(k) ~ \"=\" ~ std.uri.encodeComponent(v);\n\t\t}\n\t}\n\n\treturn ret;\n}\n\n/// Encodes all but the explicitly unreserved characters per rfc 3986\n/// Alphanumeric and -_.~ are the only ones left unencoded\n/// name is borrowed from php\nstring rawurlencode(in char[] data) {\n\tstring ret;\n\tret.reserve(data.length * 2);\n\tforeach(char c; data) {\n\t\tif(\n\t\t\t(c >= 'a' && c <= 'z') ||\n\t\t\t(c >= 'A' && c <= 'Z') ||\n\t\t\t(c >= '0' && c <= '9') ||\n\t\t\tc == '-' || c == '_' || c == '.' || c == '~')\n\t\t{\n\t\t\tret ~= c;\n\t\t} else {\n\t\t\tret ~= '%';\n\t\t\t// since we iterate on char, this should give us the octets of the full utf8 string\n\t\t\tret ~= toHexUpper(c);\n\t\t}\n\t}\n\n\treturn ret;\n}\n\n\n// http helper functions\n\n// for chunked responses (which embedded http does whenever possible)\nversion(none) // this is moved up above to avoid making a copy of the data\nconst(ubyte)[] makeChunk(const(ubyte)[] data) {\n\tconst(ubyte)[] ret;\n\n\tret = cast(const(ubyte)[]) toHex(data.length);\n\tret ~= cast(const(ubyte)[]) \"\\r\\n\";\n\tret ~= data;\n\tret ~= cast(const(ubyte)[]) \"\\r\\n\";\n\n\treturn ret;\n}\n\nstring toHex(long num) {\n\tstring ret;\n\twhile(num) {\n\t\tint v = num % 16;\n\t\tnum /= 16;\n\t\tchar d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'a');\n\t\tret ~= d;\n\t}\n\n\treturn to!string(array(ret.retro));\n}\n\nstring toHexUpper(long num) {\n\tstring ret;\n\twhile(num) {\n\t\tint v = num % 16;\n\t\tnum /= 16;\n\t\tchar d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'A');\n\t\tret ~= d;\n\t}\n\n\tif(ret.length == 1)\n\t\tret ~= \"0\"; // url encoding requires two digits and that's what this function is used for...\n\n\treturn to!string(array(ret.retro));\n}\n\n\n// the generic mixins\n\n/++\n\tUse this instead of writing your own main\n\n\tIt ultimately calls [cgiMainImpl] which creates a [RequestServer] for you.\n+/\nmixin template GenericMain(alias fun, long maxContentLength = defaultMaxContentLength) {\n\tmixin CustomCgiMain!(Cgi, fun, maxContentLength);\n}\n\n/++\n\tBoilerplate mixin for a main function that uses the [dispatcher] function.\n\n\tYou can send `typeof(null)` as the `Presenter` argument to use a generic one.\n\n\tHistory:\n\t\tAdded July 9, 2021\n+/\nmixin template DispatcherMain(Presenter, DispatcherArgs...) {\n\t/++\n\t\tHandler to the generated presenter you can use from your objects, etc.\n\t+/\n\tPresenter activePresenter;\n\n\t/++\n\t\tRequest handler that creates the presenter then forwards to the [dispatcher] function.\n\t\tRenders 404 if the dispatcher did not handle the request.\n\n\t\tWill automatically serve the presenter.style and presenter.script as \"style.css\" and \"script.js\"\n\t+/\n\tvoid handler(Cgi cgi) {\n\t\tauto presenter = new Presenter;\n\t\tactivePresenter = presenter;\n\t\tscope(exit) activePresenter = null;\n\n\t\tif(cgi.dispatcher!DispatcherArgs(presenter))\n\t\t\treturn;\n\n\t\tswitch(cgi.pathInfo) {\n\t\t\tcase \"/style.css\":\n\t\t\t\tcgi.setCache(true);\n\t\t\t\tcgi.setResponseContentType(\"text/css\");\n\t\t\t\tcgi.write(presenter.style(), true);\n\t\t\tbreak;\n\t\t\tcase \"/script.js\":\n\t\t\t\tcgi.setCache(true);\n\t\t\t\tcgi.setResponseContentType(\"application/javascript\");\n\t\t\t\tcgi.write(presenter.script(), true);\n\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tpresenter.renderBasicError(cgi, 404);\n\t\t}\n\t}\n\tmixin GenericMain!handler;\n}\n\nmixin template DispatcherMain(DispatcherArgs...) if(!is(DispatcherArgs[0] : WebPresenter!T, T)) {\n\tclass GenericPresenter : WebPresenter!GenericPresenter {}\n\tmixin DispatcherMain!(GenericPresenter, DispatcherArgs);\n}\n\nprivate string simpleHtmlEncode(string s) {\n\treturn s.replace(\"&\", \"&amp;\").replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\").replace(\"\\n\", \"<br />\\n\");\n}\n\nstring messageFromException(Throwable t) {\n\tstring message;\n\tif(t !is null) {\n\t\tdebug message = t.toString();\n\t\telse  message = \"An unexpected error has occurred.\";\n\t} else {\n\t\tmessage = \"Unknown error\";\n\t}\n\treturn message;\n}\n\nstring plainHttpError(bool isCgi, string type, Throwable t) {\n\tauto message = messageFromException(t);\n\tmessage = simpleHtmlEncode(message);\n\n\treturn format(\"%s %s\\r\\nContent-Length: %s\\r\\n\\r\\n%s\",\n\t\tisCgi ? \"Status:\" : \"HTTP/1.0\",\n\t\ttype, message.length, message);\n}\n\n// returns true if we were able to recover reasonably\nbool handleException(Cgi cgi, Throwable t) {\n\tif(cgi.isClosed) {\n\t\t// if the channel has been explicitly closed, we can't handle it here\n\t\treturn true;\n\t}\n\n\tif(cgi.outputtedResponseData) {\n\t\t// the headers are sent, but the channel is open... since it closes if all was sent, we can append an error message here.\n\t\treturn 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.\n\t} else {\n\t\t// no headers are sent, we can send a full blown error and recover\n\t\tcgi.setCache(false);\n\t\tcgi.setResponseContentType(\"text/html\");\n\t\tcgi.setResponseLocation(null); // cancel the redirect\n\t\tcgi.setResponseStatus(\"500 Internal Server Error\");\n\t\tcgi.write(simpleHtmlEncode(messageFromException(t)));\n\t\tcgi.close();\n\t\treturn true;\n\t}\n}\n\nbool isCgiRequestMethod(string s) {\n\ts = s.toUpper();\n\tif(s == \"COMMANDLINE\")\n\t\treturn true;\n\tforeach(member; __traits(allMembers, Cgi.RequestMethod))\n\t\tif(s == member)\n\t\t\treturn true;\n\treturn false;\n}\n\n/// If you want to use a subclass of Cgi with generic main, use this mixin.\nmixin template CustomCgiMain(CustomCgi, alias fun, long maxContentLength = defaultMaxContentLength) if(is(CustomCgi : Cgi)) {\n\t// kinda hacky - the T... is passed to Cgi's constructor in standard cgi mode, and ignored elsewhere\n\tvoid main(string[] args) {\n\t\tcgiMainImpl!(fun, CustomCgi, maxContentLength)(args);\n\t}\n}\n\nversion(embedded_httpd_processes)\n\t__gshared int processPoolSize = 8;\n\n// Returns true if run. You should exit the program after that.\nbool tryAddonServers(string[] args) {\n\tif(args.length > 1) {\n\t\t// run the special separate processes if needed\n\t\tswitch(args[1]) {\n\t\t\tcase \"--websocket-server\":\n\t\t\t\tversion(with_addon_servers)\n\t\t\t\t\twebsocketServers[args[2]](args[3 .. $]);\n\t\t\t\telse\n\t\t\t\t\tprintf(\"Add-on servers not compiled in.\\n\");\n\t\t\t\treturn true;\n\t\t\tcase \"--websocket-servers\":\n\t\t\t\timport core.demangle;\n\t\t\t\tversion(with_addon_servers_connections)\n\t\t\t\tforeach(k, v; websocketServers)\n\t\t\t\t\twriteln(k, \"\\t\", demangle(k));\n\t\t\t\treturn true;\n\t\t\tcase \"--session-server\":\n\t\t\t\tversion(with_addon_servers)\n\t\t\t\t\trunSessionServer();\n\t\t\t\telse\n\t\t\t\t\tprintf(\"Add-on servers not compiled in.\\n\");\n\t\t\t\treturn true;\n\t\t\tcase \"--event-server\":\n\t\t\t\tversion(with_addon_servers)\n\t\t\t\t\trunEventServer();\n\t\t\t\telse\n\t\t\t\t\tprintf(\"Add-on servers not compiled in.\\n\");\n\t\t\t\treturn true;\n\t\t\tcase \"--timer-server\":\n\t\t\t\tversion(with_addon_servers)\n\t\t\t\t\trunTimerServer();\n\t\t\t\telse\n\t\t\t\t\tprintf(\"Add-on servers not compiled in.\\n\");\n\t\t\t\treturn true;\n\t\t\tcase \"--timed-jobs\":\n\t\t\t\timport core.demangle;\n\t\t\t\tversion(with_addon_servers_connections)\n\t\t\t\tforeach(k, v; scheduledJobHandlers)\n\t\t\t\t\twriteln(k, \"\\t\", demangle(k));\n\t\t\t\treturn true;\n\t\t\tcase \"--timed-job\":\n\t\t\t\tscheduledJobHandlers[args[2]](args[3 .. $]);\n\t\t\t\treturn true;\n\t\t\tdefault:\n\t\t\t\t// intentionally blank - do nothing and carry on to run normally\n\t\t}\n\t}\n\treturn false;\n}\n\n/// Tries to simulate a request from the command line. Returns true if it does, false if it didn't find the args.\nbool trySimulatedRequest(alias fun, CustomCgi = Cgi)(string[] args) if(is(CustomCgi : Cgi)) {\n\t// we support command line thing for easy testing everywhere\n\t// it needs to be called ./app method uri [other args...]\n\tif(args.length >= 3 && isCgiRequestMethod(args[1])) {\n\t\tCgi cgi = new CustomCgi(args);\n\t\tscope(exit) cgi.dispose();\n\t\tfun(cgi);\n\t\tcgi.close();\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n/++\n\tA 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.\n\n\tHistory:\n\t\tAdded Sept 26, 2020 (release version 8.5).\n+/\nstruct RequestServer {\n\t///\n\tstring listeningHost = defaultListeningHost();\n\t///\n\tushort listeningPort = defaultListeningPort();\n\n\t/++\n\t\tUses a fork() call, if available, to provide additional crash resiliency and possibly improved performance. On the\n\t\tother hand, if you fork, you must not assume any memory is shared between requests (you shouldn't be anyway though! But\n\t\tif you have to, you probably want to set this to false and use an explicit threaded server with [serveEmbeddedHttp]) and\n\t\t[stop] may not work as well.\n\n\t\tHistory:\n\t\t\tAdded August 12, 2022  (dub v10.9). Previously, this was only configurable through the `-version=cgi_no_fork`\n\t\t\targument to dmd. That version still defines the value of `cgi_use_fork_default`, used to initialize this, for\n\t\t\tcompatibility.\n\t+/\n\tbool useFork = cgi_use_fork_default;\n\n\t/++\n\t\tDetermines the number of worker threads to spawn per process, for server modes that use worker threads. 0 will use a\n\t\tdefault based on the number of cpus modified by the server mode.\n\n\t\tHistory:\n\t\t\tAdded August 12, 2022 (dub v10.9)\n\t+/\n\tint numberOfThreads = 0;\n\n\t///\n\tthis(string defaultHost, ushort defaultPort) {\n\t\tthis.listeningHost = defaultHost;\n\t\tthis.listeningPort = defaultPort;\n\t}\n\n\t///\n\tthis(ushort defaultPort) {\n\t\tlisteningPort = defaultPort;\n\t}\n\n\t/++\n\t\tReads the command line arguments into the values here.\n\n\t\tPossible arguments are `--listening-host`, `--listening-port` (or `--port`), `--uid`, and `--gid`.\n\t+/\n\tvoid configureFromCommandLine(string[] args) {\n\t\tbool foundPort = false;\n\t\tbool foundHost = false;\n\t\tbool foundUid = false;\n\t\tbool foundGid = false;\n\t\tforeach(arg; args) {\n\t\t\tif(foundPort) {\n\t\t\t\tlisteningPort = to!ushort(arg);\n\t\t\t\tfoundPort = false;\n\t\t\t}\n\t\t\tif(foundHost) {\n\t\t\t\tlisteningHost = arg;\n\t\t\t\tfoundHost = false;\n\t\t\t}\n\t\t\tif(foundUid) {\n\t\t\t\tprivilegesDropToUid = to!uid_t(arg);\n\t\t\t\tfoundUid = false;\n\t\t\t}\n\t\t\tif(foundGid) {\n\t\t\t\tprivilegesDropToGid = to!gid_t(arg);\n\t\t\t\tfoundGid = false;\n\t\t\t}\n\t\t\tif(arg == \"--listening-host\" || arg == \"-h\" || arg == \"/listening-host\")\n\t\t\t\tfoundHost = true;\n\t\t\telse if(arg == \"--port\" || arg == \"-p\" || arg == \"/port\" || arg == \"--listening-port\")\n\t\t\t\tfoundPort = true;\n\t\t\telse if(arg == \"--uid\")\n\t\t\t\tfoundUid = true;\n\t\t\telse if(arg == \"--gid\")\n\t\t\t\tfoundGid = true;\n\t\t}\n\t}\n\n\tversion(Windows) {\n\t\tprivate alias uid_t = int;\n\t\tprivate alias gid_t = int;\n\t}\n\n\t/// user (uid) to drop privileges to\n\t/// 0 … do nothing\n\tuid_t privilegesDropToUid = 0;\n\t/// group (gid) to drop privileges to\n\t/// 0 … do nothing\n\tgid_t privilegesDropToGid = 0;\n\n\tprivate void dropPrivileges() {\n\t\tversion(Posix) {\n\t\t\timport core.sys.posix.unistd;\n\n\t\t\tif (privilegesDropToGid != 0 && setgid(privilegesDropToGid) != 0)\n\t\t\t\tthrow new Exception(\"Dropping privileges via setgid() failed.\");\n\n\t\t\tif (privilegesDropToUid != 0 && setuid(privilegesDropToUid) != 0)\n\t\t\t\tthrow new Exception(\"Dropping privileges via setuid() failed.\");\n\t\t}\n\t\telse {\n\t\t\t// FIXME: Windows?\n\t\t\t//pragma(msg, \"Dropping privileges is not implemented for this platform\");\n\t\t}\n\n\t\t// done, set zero\n\t\tprivilegesDropToGid = 0;\n\t\tprivilegesDropToUid = 0;\n\t}\n\n\t/++\n\t\tServes a single HTTP request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders\n\n\t\tHistory:\n\t\t\tAdded Oct 10, 2020.\n\t\tExample:\n\n\t\t---\n\t\timport arsd.cgi;\n\t\tvoid main() {\n\t\t\tRequestServer server = RequestServer(\"127.0.0.1\", 6789);\n\t\t\tstring oauthCode;\n\t\t\tstring oauthScope;\n\t\t\tserver.serveHttpOnce!((cgi) {\n\t\t\t\toauthCode = cgi.request(\"code\");\n\t\t\t\toauthScope = cgi.request(\"scope\");\n\t\t\t\tcgi.write(\"Thank you, please return to the application.\");\n\t\t\t});\n\t\t\t// use the code and scope given\n\t\t}\n\t\t---\n\t+/\n\tvoid serveHttpOnce(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {\n\t\timport std.socket;\n\n\t\tbool tcp;\n\t\tvoid delegate() cleanup;\n\t\tauto socket = startListening(listeningHost, listeningPort, tcp, cleanup, 1, &dropPrivileges);\n\t\tauto connection = socket.accept();\n\t\tdoThreadHttpConnectionGuts!(CustomCgi, fun, true)(connection);\n\n\t\tif(cleanup)\n\t\t\tcleanup();\n\t}\n\n\t/++\n\t\tStarts serving requests according to the current configuration.\n\t+/\n\tvoid serve(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {\n\t\tversion(netman_httpd) {\n\t\t\t// Obsolete!\n\n\t\t\timport arsd.httpd;\n\t\t\t// what about forwarding the other constructor args?\n\t\t\t// this probably needs a whole redoing...\n\t\t\tserveHttp!CustomCgi(&fun, listeningPort);//5005);\n\t\t\treturn;\n\t\t} else\n\t\tversion(embedded_httpd_processes) {\n\t\t\tserveEmbeddedHttpdProcesses!(fun, CustomCgi)(this);\n\t\t} else\n\t\tversion(embedded_httpd_threads) {\n\t\t\tserveEmbeddedHttp!(fun, CustomCgi, maxContentLength)();\n\t\t} else\n\t\tversion(scgi) {\n\t\t\tserveScgi!(fun, CustomCgi, maxContentLength)();\n\t\t} else\n\t\tversion(fastcgi) {\n\t\t\tserveFastCgi!(fun, CustomCgi, maxContentLength)(this);\n\t\t} else\n\t\tversion(stdio_http) {\n\t\t\tserveSingleHttpConnectionOnStdio!(fun, CustomCgi, maxContentLength)();\n\t\t} else {\n\t\t\t//version=plain_cgi;\n\t\t\thandleCgiRequest!(fun, CustomCgi, maxContentLength)();\n\t\t}\n\t}\n\n\t/++\n\t\tRuns the embedded HTTP thread server specifically, regardless of which build configuration you have.\n\n\t\tIf you want the forking worker process server, you do need to compile with the embedded_httpd_processes config though.\n\t+/\n\tshared void serveEmbeddedHttp(alias fun, T, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(shared T _this) {\n\t\tglobalStopFlag = false;\n\t\tstatic if(__traits(isStaticFunction, fun))\n\t\t\tvoid funToUse(CustomCgi cgi) {\n\t\t\t\tfun(_this, cgi);\n\t\t\t}\n\t\telse\n\t\t\tvoid funToUse(CustomCgi cgi) {\n\t\t\t\tstatic if(__VERSION__ > 2097)\n\t\t\t\t\t__traits(child, _inst_this, fun)(_inst_this, cgi);\n\t\t\t\telse static assert(0, \"Not implemented in your compiler version!\");\n\t\t\t}\n\t\tauto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads);\n\t\tmanager.listen();\n\t}\n\n\t/++\n\t\tRuns the embedded SCGI server specifically, regardless of which build configuration you have.\n\t+/\n\tvoid serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {\n\t\tglobalStopFlag = false;\n\t\tauto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads);\n\t\tmanager.listen();\n\t}\n\n\t/++\n\t\tServes a single \"connection\", but the connection is spoken on stdin and stdout instead of on a socket.\n\n\t\tIntended for cases like working from systemd, like discussed here: [https://forum.dlang.org/post/avmkfdiitirnrenzljwc@forum.dlang.org]\n\n\t\tHistory:\n\t\t\tAdded May 29, 2021\n\t+/\n\tvoid serveSingleHttpConnectionOnStdio(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {\n\t\tdoThreadHttpConnectionGuts!(CustomCgi, fun, true)(new FakeSocketForStdin());\n\t}\n\n\t/++\n\t\tThe [stop] function sets a flag that request handlers can (and should) check periodically. If a handler doesn't\n\t\trespond to this flag, the library will force the issue. This determines when and how the issue will be forced.\n\t+/\n\tenum ForceStop {\n\t\t/++\n\t\t\tStops accepting new requests, but lets ones already in the queue start and complete before exiting.\n\t\t+/\n\t\tafterQueuedRequestsComplete,\n\t\t/++\n\t\t\tFinishes requests already started their handlers, but drops any others in the queue. Streaming handlers\n\t\t\tshould cooperate and exit gracefully, but if they don't, it will continue waiting for them.\n\t\t+/\n\t\tafterCurrentRequestsComplete,\n\t\t/++\n\t\t\tPartial response writes will throw an exception, cancelling any streaming response, but complete\n\t\t\twrites will continue to process. Request handlers that respect the stop token will also gracefully cancel.\n\t\t+/\n\t\tcancelStreamingRequestsEarly,\n\t\t/++\n\t\t\tAll writes will throw.\n\t\t+/\n\t\tcancelAllRequestsEarly,\n\t\t/++\n\t\t\tUse OS facilities to forcibly kill running threads. The server process will be in an undefined state after this call (if this call ever returns).\n\t\t+/\n\t\tforciblyTerminate,\n\t}\n\n\tversion(embedded_httpd_processes) {} else\n\t/++\n\t\tStops serving after the current requests are completed.\n\n\t\tBugs:\n\t\t\tNot implemented on version=embedded_httpd_processes, version=fastcgi on any system, or embedded_httpd on Windows (it does work on embedded_httpd_hybrid\n\t\t\ton Windows however). Only partially implemented on non-Linux posix systems.\n\n\t\t\tYou might also try SIGINT perhaps.\n\n\t\t\tThe stopPriority is not yet fully implemented.\n\t+/\n\tstatic void stop(ForceStop stopPriority = ForceStop.afterCurrentRequestsComplete) {\n\t\tglobalStopFlag = true;\n\n\t\tversion(Posix) {\n\t\t\tif(cancelfd > 0) {\n\t\t\t\tulong a = 1;\n\t\t\t\tcore.sys.posix.unistd.write(cancelfd, &a, a.sizeof);\n\t\t\t}\n\t\t}\n\t\tversion(Windows) {\n\t\t\tif(iocp) {\n\t\t\t\tforeach(i; 0 .. 16) // FIXME\n\t\t\t\tPostQueuedCompletionStatus(iocp, 0, cast(ULONG_PTR) null, null);\n\t\t\t}\n\t\t}\n\t}\n}\n\nprivate alias AliasSeq(T...) = T;\n\nversion(with_breaking_cgi_features)\nmixin(q{\n\ttemplate ThisFor(alias t) {\n\t\tstatic if(__traits(isStaticFunction, t)) {\n\t\t\talias ThisFor = AliasSeq!();\n\t\t} else {\n\t\t\talias ThisFor = __traits(parent, t);\n\t\t}\n\t}\n});\nelse\n\talias ThisFor(alias t) = AliasSeq!();\n\nprivate __gshared bool globalStopFlag = false;\n\nversion(embedded_httpd_processes)\nvoid serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer params) {\n\timport core.sys.posix.unistd;\n\timport core.sys.posix.sys.socket;\n\timport core.sys.posix.netinet.in_;\n\t//import std.c.linux.socket;\n\n\tint sock = socket(AF_INET, SOCK_STREAM, 0);\n\tif(sock == -1)\n\t\tthrow new Exception(\"socket\");\n\n\tcloexec(sock);\n\n\t{\n\n\t\tsockaddr_in addr;\n\t\taddr.sin_family = AF_INET;\n\t\taddr.sin_port = htons(params.listeningPort);\n\t\tauto lh = params.listeningHost;\n\t\tif(lh.length) {\n\t\t\tif(inet_pton(AF_INET, lh.toStringz(), &addr.sin_addr.s_addr) != 1)\n\t\t\t\tthrow 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.\");\n\t\t} else\n\t\t\taddr.sin_addr.s_addr = INADDR_ANY;\n\n\t\t// HACKISH\n\t\tint on = 1;\n\t\tsetsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, on.sizeof);\n\t\t// end hack\n\n\n\t\tif(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) {\n\t\t\tclose(sock);\n\t\t\tthrow new Exception(\"bind\");\n\t\t}\n\n\t\t// FIXME: if this queue is full, it will just ignore it\n\t\t// and wait for the client to retransmit it. This is an\n\t\t// obnoxious timeout condition there.\n\t\tif(sock.listen(128) == -1) {\n\t\t\tclose(sock);\n\t\t\tthrow new Exception(\"listen\");\n\t\t}\n\t\tparams.dropPrivileges();\n\t}\n\n\tversion(embedded_httpd_processes_accept_after_fork) {} else {\n\t\tint pipeReadFd;\n\t\tint pipeWriteFd;\n\n\t\t{\n\t\t\tint[2] pipeFd;\n\t\t\tif(socketpair(AF_UNIX, SOCK_DGRAM, 0, pipeFd)) {\n\t\t\t\timport core.stdc.errno;\n\t\t\t\tthrow new Exception(\"pipe failed \" ~ to!string(errno));\n\t\t\t}\n\n\t\t\tpipeReadFd = pipeFd[0];\n\t\t\tpipeWriteFd = pipeFd[1];\n\t\t}\n\t}\n\n\n\tint processCount;\n\tpid_t newPid;\n\treopen:\n\twhile(processCount < processPoolSize) {\n\t\tnewPid = fork();\n\t\tif(newPid == 0) {\n\t\t\t// start serving on the socket\n\t\t\t//ubyte[4096] backingBuffer;\n\t\t\tfor(;;) {\n\t\t\t\tbool closeConnection;\n\t\t\t\tuint i;\n\t\t\t\tsockaddr addr;\n\t\t\t\ti = addr.sizeof;\n\t\t\t\tversion(embedded_httpd_processes_accept_after_fork) {\n\t\t\t\t\tint s = accept(sock, &addr, &i);\n\t\t\t\t\tint opt = 1;\n\t\t\t\t\timport core.sys.posix.netinet.tcp;\n\t\t\t\t\t// the Cgi class does internal buffering, so disabling this\n\t\t\t\t\t// helps with latency in many cases...\n\t\t\t\t\tsetsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof);\n\t\t\t\t\tcloexec(s);\n\t\t\t\t} else {\n\t\t\t\t\tint s;\n\t\t\t\t\tauto readret = read_fd(pipeReadFd, &s, s.sizeof, &s);\n\t\t\t\t\tif(readret != s.sizeof) {\n\t\t\t\t\t\timport core.stdc.errno;\n\t\t\t\t\t\tthrow new Exception(\"pipe read failed \" ~ to!string(errno));\n\t\t\t\t\t}\n\n\t\t\t\t\t//writeln(\"process \", getpid(), \" got socket \", s);\n\t\t\t\t}\n\n\t\t\t\ttry {\n\n\t\t\t\t\tif(s == -1)\n\t\t\t\t\t\tthrow new Exception(\"accept\");\n\n\t\t\t\t\tscope(failure) close(s);\n\t\t\t\t\t//ubyte[__traits(classInstanceSize, BufferedInputRange)] bufferedRangeContainer;\n\t\t\t\t\tauto ir = new BufferedInputRange(s);\n\t\t\t\t\t//auto ir = emplace!BufferedInputRange(bufferedRangeContainer, s, backingBuffer);\n\n\t\t\t\t\twhile(!ir.empty) {\n\t\t\t\t\t\t//ubyte[__traits(classInstanceSize, CustomCgi)] cgiContainer;\n\n\t\t\t\t\t\tCgi cgi;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tcgi = new CustomCgi(ir, &closeConnection);\n\t\t\t\t\t\t\tcgi._outputFileHandle = cast(CgiConnectionHandle) s;\n\t\t\t\t\t\t\t// 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.\n\t\t\t\t\t\t\tif(processPoolSize <= 1)\n\t\t\t\t\t\t\t\tcloseConnection = true;\n\t\t\t\t\t\t\t//cgi = emplace!CustomCgi(cgiContainer, ir, &closeConnection);\n\t\t\t\t\t\t} catch(Throwable t) {\n\t\t\t\t\t\t\t// a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P\n\t\t\t\t\t\t\t// anyway let's kill the connection\n\t\t\t\t\t\t\tversion(CRuntime_Musl) {\n\t\t\t\t\t\t\t\t// LockingTextWriter fails here\n\t\t\t\t\t\t\t\t// so working around it\n\t\t\t\t\t\t\t\tauto estr = t.toString();\n\t\t\t\t\t\t\t\tstderr.rawWrite(estr);\n\t\t\t\t\t\t\t\tstderr.rawWrite(\"\\n\");\n\t\t\t\t\t\t\t} else\n\t\t\t\t\t\t\t\tstderr.writeln(t.toString());\n\t\t\t\t\t\t\tsendAll(ir.source, plainHttpError(false, \"400 Bad Request\", t));\n\t\t\t\t\t\t\tcloseConnection = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tassert(cgi !is null);\n\t\t\t\t\t\tscope(exit)\n\t\t\t\t\t\t\tcgi.dispose();\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tfun(cgi);\n\t\t\t\t\t\t\tcgi.close();\n\t\t\t\t\t\t\tif(cgi.websocketMode)\n\t\t\t\t\t\t\t\tcloseConnection = true;\n\t\t\t\t\t\t} catch(ConnectionException ce) {\n\t\t\t\t\t\t\tcloseConnection = true;\n\t\t\t\t\t\t} catch(Throwable t) {\n\t\t\t\t\t\t\t// a processing error can be recovered from\n\t\t\t\t\t\t\tversion(CRuntime_Musl) {\n\t\t\t\t\t\t\t\t// LockingTextWriter fails here\n\t\t\t\t\t\t\t\t// so working around it\n\t\t\t\t\t\t\t\tauto estr = t.toString();\n\t\t\t\t\t\t\t\tstderr.rawWrite(estr);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tstderr.writeln(t.toString);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(!handleException(cgi, t))\n\t\t\t\t\t\t\t\tcloseConnection = true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif(closeConnection) {\n\t\t\t\t\t\t\tir.source.close();\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif(!ir.empty)\n\t\t\t\t\t\t\t\tir.popFront(); // get the next\n\t\t\t\t\t\t\telse if(ir.sourceClosed) {\n\t\t\t\t\t\t\t\tir.source.close();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tir.source.close();\n\t\t\t\t} catch(Throwable t) {\n\t\t\t\t\tversion(CRuntime_Musl) {} else\n\t\t\t\t\t\tdebug writeln(t);\n\t\t\t\t\t// most likely cause is a timeout\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(newPid < 0) {\n\t\t\tthrow new Exception(\"fork failed\");\n\t\t} else {\n\t\t\tprocessCount++;\n\t\t}\n\t}\n\n\t// the parent should wait for its children...\n\tif(newPid) {\n\t\timport core.sys.posix.sys.wait;\n\n\t\tversion(embedded_httpd_processes_accept_after_fork) {} else {\n\t\t\timport core.sys.posix.sys.select;\n\t\t\tint[] fdQueue;\n\t\t\twhile(true) {\n\t\t\t\t// writeln(\"select call\");\n\t\t\t\tint nfds = pipeWriteFd;\n\t\t\t\tif(sock > pipeWriteFd)\n\t\t\t\t\tnfds = sock;\n\t\t\t\tnfds += 1;\n\t\t\t\tfd_set read_fds;\n\t\t\t\tfd_set write_fds;\n\t\t\t\tFD_ZERO(&read_fds);\n\t\t\t\tFD_ZERO(&write_fds);\n\t\t\t\tFD_SET(sock, &read_fds);\n\t\t\t\tif(fdQueue.length)\n\t\t\t\t\tFD_SET(pipeWriteFd, &write_fds);\n\t\t\t\tauto ret = select(nfds, &read_fds, &write_fds, null, null);\n\t\t\t\tif(ret == -1) {\n\t\t\t\t\timport core.stdc.errno;\n\t\t\t\t\tif(errno == EINTR)\n\t\t\t\t\t\tgoto try_wait;\n\t\t\t\t\telse\n\t\t\t\t\t\tthrow new Exception(\"wtf select\");\n\t\t\t\t}\n\n\t\t\t\tint s = -1;\n\t\t\t\tif(FD_ISSET(sock, &read_fds)) {\n\t\t\t\t\tuint i;\n\t\t\t\t\tsockaddr addr;\n\t\t\t\t\ti = addr.sizeof;\n\t\t\t\t\ts = accept(sock, &addr, &i);\n\t\t\t\t\tcloexec(s);\n\t\t\t\t\timport core.sys.posix.netinet.tcp;\n\t\t\t\t\tint opt = 1;\n\t\t\t\t\tsetsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof);\n\t\t\t\t}\n\n\t\t\t\tif(FD_ISSET(pipeWriteFd, &write_fds)) {\n\t\t\t\t\tif(s == -1 && fdQueue.length) {\n\t\t\t\t\t\ts = fdQueue[0];\n\t\t\t\t\t\tfdQueue = fdQueue[1 .. $]; // FIXME reuse buffer\n\t\t\t\t\t}\n\t\t\t\t\twrite_fd(pipeWriteFd, &s, s.sizeof, s);\n\t\t\t\t\tclose(s); // we are done with it, let the other process take ownership\n\t\t\t\t} else\n\t\t\t\t\tfdQueue ~= s;\n\t\t\t}\n\t\t}\n\n\t\ttry_wait:\n\n\t\tint status;\n\t\twhile(-1 != wait(&status)) {\n\t\t\tversion(CRuntime_Musl) {} else {\n\t\t\t\timport std.stdio; writeln(\"Process died \", status);\n\t\t\t}\n\t\t\tprocessCount--;\n\t\t\tgoto reopen;\n\t\t}\n\t\tclose(sock);\n\t}\n}\n\nversion(fastcgi)\nvoid serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(RequestServer params) {\n\t//         SetHandler fcgid-script\n\tFCGX_Stream* input, output, error;\n\tFCGX_ParamArray env;\n\n\n\n\tconst(ubyte)[] getFcgiChunk() {\n\t\tconst(ubyte)[] ret;\n\t\twhile(FCGX_HasSeenEOF(input) != -1)\n\t\t\tret ~= cast(ubyte) FCGX_GetChar(input);\n\t\treturn ret;\n\t}\n\n\tvoid writeFcgi(const(ubyte)[] data) {\n\t\tFCGX_PutStr(data.ptr, data.length, output);\n\t}\n\n\tvoid doARequest() {\n\t\tstring[string] fcgienv;\n\n\t\tfor(auto e = env; e !is null && *e !is null; e++) {\n\t\t\tstring cur = to!string(*e);\n\t\t\tauto idx = cur.indexOf(\"=\");\n\t\t\tstring name, value;\n\t\t\tif(idx == -1)\n\t\t\t\tname = cur;\n\t\t\telse {\n\t\t\t\tname = cur[0 .. idx];\n\t\t\t\tvalue = cur[idx + 1 .. $];\n\t\t\t}\n\n\t\t\tfcgienv[name] = value;\n\t\t}\n\n\t\tvoid flushFcgi() {\n\t\t\tFCGX_FFlush(output);\n\t\t}\n\n\t\tCgi cgi;\n\t\ttry {\n\t\t\tcgi = new CustomCgi(maxContentLength, fcgienv, &getFcgiChunk, &writeFcgi, &flushFcgi);\n\t\t} catch(Throwable t) {\n\t\t\tFCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error);\n\t\t\twriteFcgi(cast(const(ubyte)[]) plainHttpError(true, \"400 Bad Request\", t));\n\t\t\treturn; //continue;\n\t\t}\n\t\tassert(cgi !is null);\n\t\tscope(exit) cgi.dispose();\n\t\ttry {\n\t\t\tfun(cgi);\n\t\t\tcgi.close();\n\t\t} catch(Throwable t) {\n\t\t\t// log it to the error stream\n\t\t\tFCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error);\n\t\t\t// handle it for the user, if we can\n\t\t\tif(!handleException(cgi, t))\n\t\t\t\treturn; // continue;\n\t\t}\n\t}\n\n\tauto lp = params.listeningPort;\n\tauto host = params.listeningHost;\n\n\tFCGX_Request request;\n\tif(lp || !host.empty) {\n\t\t// if a listening port was specified on the command line, we want to spawn ourself\n\t\t// (needed for nginx without spawn-fcgi, e.g. on Windows)\n\t\tFCGX_Init();\n\n\t\tint sock;\n\n\t\tif(host.startsWith(\"unix:\")) {\n\t\t\tsock = FCGX_OpenSocket(toStringz(params.listeningHost[\"unix:\".length .. $]), 12);\n\t\t} else if(host.startsWith(\"abstract:\")) {\n\t\t\tsock = FCGX_OpenSocket(toStringz(\"\\0\" ~ params.listeningHost[\"abstract:\".length .. $]), 12);\n\t\t} else {\n\t\t\tsock = FCGX_OpenSocket(toStringz(params.listeningHost ~ \":\" ~ to!string(lp)), 12);\n\t\t}\n\n\t\tif(sock < 0)\n\t\t\tthrow new Exception(\"Couldn't listen on the port\");\n\t\tFCGX_InitRequest(&request, sock, 0);\n\t\twhile(FCGX_Accept_r(&request) >= 0) {\n\t\t\tinput = request.inStream;\n\t\t\toutput = request.outStream;\n\t\t\terror = request.errStream;\n\t\t\tenv = request.envp;\n\t\t\tdoARequest();\n\t\t}\n\t} else {\n\t\t// otherwise, assume the httpd is doing it (the case for Apache, IIS, and Lighttpd)\n\t\t// using the version with a global variable since we are separate processes anyway\n\t\twhile(FCGX_Accept(&input, &output, &error, &env) >= 0) {\n\t\t\tdoARequest();\n\t\t}\n\t}\n}\n\n/// Returns the default listening port for the current cgi configuration. 8085 for embedded httpd, 4000 for scgi, irrelevant for others.\nushort defaultListeningPort() {\n\tversion(netman_httpd)\n\t\treturn 8080;\n\telse version(embedded_httpd_processes)\n\t\treturn 8085;\n\telse version(embedded_httpd_threads)\n\t\treturn 8085;\n\telse version(scgi)\n\t\treturn 4000;\n\telse\n\t\treturn 0;\n}\n\n/// 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.\nstring defaultListeningHost() {\n\tversion(netman_httpd)\n\t\treturn null;\n\telse version(embedded_httpd_processes)\n\t\treturn null;\n\telse version(embedded_httpd_threads)\n\t\treturn null;\n\telse version(scgi)\n\t\treturn \"127.0.0.1\";\n\telse\n\t\treturn null;\n\n}\n\n/++\n\tThis 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`.\n\n\tPlease 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).\n\n\tParams:\n\t\tfun = Your request handler\n\t\tCustomCgi = a subclass of Cgi, if you wise to customize it further\n\t\tmaxContentLength = max POST size you want to allow\n\t\targs = command-line arguments\n\n\tHistory:\n\t\tDocumented Sept 26, 2020.\n+/\nvoid cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(string[] args) if(is(CustomCgi : Cgi)) {\n\tif(tryAddonServers(args))\n\t\treturn;\n\n\tif(trySimulatedRequest!(fun, CustomCgi)(args))\n\t\treturn;\n\n\tRequestServer server;\n\t// you can change the port here if you like\n\t// server.listeningPort = 9000;\n\n\t// then call this to let the command line args override your default\n\tserver.configureFromCommandLine(args);\n\n\t// and serve the request(s).\n\tserver.serve!(fun, CustomCgi, maxContentLength)();\n}\n\n//version(plain_cgi)\nvoid handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() {\n\t// standard CGI is the default version\n\n\n\t// Set stdin to binary mode if necessary to avoid mangled newlines\n\t// the fact that stdin is global means this could be trouble but standard cgi request\n\t// handling is one per process anyway so it shouldn't actually be threaded here or anything.\n\tversion(Windows) {\n\t\tversion(Win64)\n\t\t_setmode(std.stdio.stdin.fileno(), 0x8000);\n\t\telse\n\t\tsetmode(std.stdio.stdin.fileno(), 0x8000);\n\t}\n\n\tCgi cgi;\n\ttry {\n\t\tcgi = new CustomCgi(maxContentLength);\n\t\tversion(Posix)\n\t\t\tcgi._outputFileHandle = cast(CgiConnectionHandle) 1; // stdout\n\t\telse version(Windows)\n\t\t\tcgi._outputFileHandle = cast(CgiConnectionHandle) GetStdHandle(STD_OUTPUT_HANDLE);\n\t\telse static assert(0);\n\t} catch(Throwable t) {\n\t\tversion(CRuntime_Musl) {\n\t\t\t// LockingTextWriter fails here\n\t\t\t// so working around it\n\t\t\tauto s = t.toString();\n\t\t\tstderr.rawWrite(s);\n\t\t\tstdout.rawWrite(plainHttpError(true, \"400 Bad Request\", t));\n\t\t} else {\n\t\t\tstderr.writeln(t.msg);\n\t\t\t// the real http server will probably handle this;\n\t\t\t// most likely, this is a bug in Cgi. But, oh well.\n\t\t\tstdout.write(plainHttpError(true, \"400 Bad Request\", t));\n\t\t}\n\t\treturn;\n\t}\n\tassert(cgi !is null);\n\tscope(exit) cgi.dispose();\n\n\ttry {\n\t\tfun(cgi);\n\t\tcgi.close();\n\t} catch (Throwable t) {\n\t\tversion(CRuntime_Musl) {\n\t\t\t// LockingTextWriter fails here\n\t\t\t// so working around it\n\t\t\tauto s = t.msg;\n\t\t\tstderr.rawWrite(s);\n\t\t} else {\n\t\t\tstderr.writeln(t.msg);\n\t\t}\n\t\tif(!handleException(cgi, t))\n\t\t\treturn;\n\t}\n}\n\nprivate __gshared int cancelfd = -1;\n\n/+\n\tThe event loop for embedded_httpd_threads will prolly fiber dispatch\n\tcgi constructors too, so slow posts will not monopolize a worker thread.\n\n\tMay want to provide the worker task system just need to ensure all the fibers\n\thas a big enough stack for real work... would also ideally like to reuse them.\n\n\n\tSo prolly bir would switch it to nonblocking. If it would block, it epoll\n\tregisters one shot with this existing fiber to take it over.\n\n\t\tnew connection comes in. it picks a fiber off the free list,\n\t\tor if there is none, it creates a new one. this fiber handles\n\t\tthis connection the whole time.\n\n\t\tepoll triggers the fiber when something comes in. it is called by\n\t\ta random worker thread, it might change at any time. at least during\n\t\tthe constructor. maybe into the main body it will stay tied to a thread\n\t\tjust so TLS stuff doesn't randomly change in the middle. but I could\n\t\tspecify if you yield all bets are off.\n\n\t\twhen the request is finished, if there's more data buffered, it just\n\t\tkeeps going. if there is no more data buffered, it epoll ctls to\n\t\tget triggered when more data comes in. all one shot.\n\n\t\twhen a connection is closed, the fiber returns and is then reset\n\t\tand added to the free list. if the free list is full, the fiber is\n\t\tjust freed, this means it will balloon to a certain size but not generally\n\t\tgrow beyond that unless the activity keeps going.\n\n\t\t256 KB stack i thnk per fiber. 4,000 active fibers per gigabyte of memory.\n\n\tSo the fiber has its own magic methods to read and write. if they would block, it registers\n\tfor epoll and yields. when it returns, it read/writes and then returns back normal control.\n\n\tbasically you issue the command and it tells you when it is done\n\n\tit needs to DEL the epoll thing when it is closed. add it when opened. mod it when anther thing issued\n\n+/\n\n/++\n\tThe stack size when a fiber is created. You can set this from your main or from a shared static constructor\n\tto optimize your memory use if you know you don't need this much space. Be careful though, some functions use\n\tmore stack space than you realize and a recursive function (including ones like in dom.d) can easily grow fast!\n\n\tHistory:\n\t\tAdded July 10, 2021. Previously, it used the druntime default of 16 KB.\n+/\nversion(cgi_use_fiber)\n__gshared size_t fiberStackSize = 4096 * 100;\n\nversion(cgi_use_fiber)\nclass CgiFiber : Fiber {\n\tprivate void function(Socket) f_handler;\n\tprivate void f_handler_dg(Socket s) { // to avoid extra allocation w/ function\n\t\tf_handler(s);\n\t}\n\tthis(void function(Socket) handler) {\n\t\tthis.f_handler = handler;\n\t\tthis(&f_handler_dg);\n\t}\n\n\tthis(void delegate(Socket) handler) {\n\t\tthis.handler = handler;\n\t\tsuper(&run, fiberStackSize);\n\t}\n\n\tSocket connection;\n\tvoid delegate(Socket) handler;\n\n\tvoid run() {\n\t\thandler(connection);\n\t}\n\n\tvoid delegate() postYield;\n\n\tprivate void setPostYield(scope void delegate() py) @nogc {\n\t\tpostYield = cast(void delegate()) py;\n\t}\n\n\tvoid proceed() {\n\t\ttry {\n\t\t\tcall();\n\t\t\tauto py = postYield;\n\t\t\tpostYield = null;\n\t\t\tif(py !is null)\n\t\t\t\tpy();\n\t\t} catch(Exception e) {\n\t\t\tif(connection)\n\t\t\t\tconnection.close();\n\t\t\tgoto terminate;\n\t\t}\n\n\t\tif(state == State.TERM) {\n\t\t\tterminate:\n\t\t\timport core.memory;\n\t\t\tGC.removeRoot(cast(void*) this);\n\t\t}\n\t}\n}\n\nversion(cgi_use_fiber)\nversion(Windows) {\n\nextern(Windows) private {\n\n\timport core.sys.windows.mswsock;\n\n\talias GROUP=uint;\n\talias LPWSAPROTOCOL_INFOW = void*;\n\tSOCKET WSASocketW(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags);\n\tint WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);\n\tint WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);\n\n\tstruct WSABUF {\n\t\tULONG len;\n\t\tCHAR  *buf;\n\t}\n\talias LPWSABUF = WSABUF*;\n\n\talias WSAOVERLAPPED = OVERLAPPED;\n\talias LPWSAOVERLAPPED = LPOVERLAPPED;\n\t/+\n\n\talias LPFN_ACCEPTEX =\n\t\tBOOL\n\t\tfunction(\n\t\t\t\tSOCKET sListenSocket,\n\t\t\t\tSOCKET sAcceptSocket,\n\t\t\t\t//_Out_writes_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer,\n\t\t\t\tvoid* lpOutputBuffer,\n\t\t\t\tWORD dwReceiveDataLength,\n\t\t\t\tWORD dwLocalAddressLength,\n\t\t\t\tWORD dwRemoteAddressLength,\n\t\t\t\tLPDWORD lpdwBytesReceived,\n\t\t\t\tLPOVERLAPPED lpOverlapped\n\t\t\t);\n\n\tenum WSAID_ACCEPTEX = GUID([0xb5367df1,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]]);\n\t+/\n\n\tenum WSAID_GETACCEPTEXSOCKADDRS = GUID(0xb5367df2,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]);\n}\n\nprivate class PseudoblockingOverlappedSocket : Socket {\n\tSOCKET handle;\n\n\tCgiFiber fiber;\n\n\tthis(AddressFamily af, SocketType st) {\n\t\tauto handle = WSASocketW(af, st, 0, null, 0, 1 /*WSA_FLAG_OVERLAPPED*/);\n\t\tif(!handle)\n\t\t\tthrow new Exception(\"WSASocketW\");\n\t\tthis.handle = handle;\n\n\t\tiocp = CreateIoCompletionPort(cast(HANDLE) handle, iocp, cast(ULONG_PTR) cast(void*) this, 0);\n\n\t\tif(iocp is null) {\n\t\t\twriteln(GetLastError());\n\t\t\tthrow new Exception(\"CreateIoCompletionPort\");\n\t\t}\n\n\t\tsuper(cast(socket_t) handle, af);\n\t}\n\tthis() pure nothrow @trusted { assert(0); }\n\n\toverride void blocking(bool) {} // meaningless to us, just ignore it.\n\n\tprotected override Socket accepting() pure nothrow {\n\t\tassert(0);\n\t}\n\n\tbool addressesParsed;\n\tAddress la;\n\tAddress ra;\n\n\tprivate void populateAddresses() {\n\t\tif(addressesParsed)\n\t\t\treturn;\n\t\taddressesParsed = true;\n\n\t\tint lalen, ralen;\n\n\t\tsockaddr_in* la;\n\t\tsockaddr_in* ra;\n\n\t\tlpfnGetAcceptExSockaddrs(\n\t\t\tscratchBuffer.ptr,\n\t\t\t0, // same as in the AcceptEx call!\n\t\t\tsockaddr_in.sizeof + 16,\n\t\t\tsockaddr_in.sizeof + 16,\n\t\t\tcast(sockaddr**) &la,\n\t\t\t&lalen,\n\t\t\tcast(sockaddr**) &ra,\n\t\t\t&ralen\n\t\t);\n\n\t\tif(la)\n\t\t\tthis.la = new InternetAddress(*la);\n\t\tif(ra)\n\t\t\tthis.ra = new InternetAddress(*ra);\n\n\t}\n\n\toverride @property @trusted Address localAddress() {\n\t\tpopulateAddresses();\n\t\treturn la;\n\t}\n\toverride @property @trusted Address remoteAddress() {\n\t\tpopulateAddresses();\n\t\treturn ra;\n\t}\n\n\tPseudoblockingOverlappedSocket accepted;\n\n\t__gshared static LPFN_ACCEPTEX lpfnAcceptEx;\n\t__gshared static typeof(&GetAcceptExSockaddrs) lpfnGetAcceptExSockaddrs;\n\n\toverride Socket accept() @trusted {\n\t\t__gshared static LPFN_ACCEPTEX lpfnAcceptEx;\n\n\t\tif(lpfnAcceptEx is null) {\n\t\t\tDWORD dwBytes;\n\t\t\tGUID GuidAcceptEx = WSAID_ACCEPTEX;\n\n\t\t\tauto iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/,\n\t\t\t\t\t&GuidAcceptEx, GuidAcceptEx.sizeof,\n\t\t\t\t\t&lpfnAcceptEx, lpfnAcceptEx.sizeof,\n\t\t\t\t\t&dwBytes, null, null);\n\n\t\t\tGuidAcceptEx = WSAID_GETACCEPTEXSOCKADDRS;\n\t\t\tiResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/,\n\t\t\t\t\t&GuidAcceptEx, GuidAcceptEx.sizeof,\n\t\t\t\t\t&lpfnGetAcceptExSockaddrs, lpfnGetAcceptExSockaddrs.sizeof,\n\t\t\t\t\t&dwBytes, null, null);\n\n\t\t}\n\n\t\tauto pfa = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM);\n\t\taccepted = pfa;\n\n\t\tSOCKET pendingForAccept = pfa.handle;\n\t\tDWORD ignored;\n\n\t\tauto ret = lpfnAcceptEx(handle,\n\t\t\tpendingForAccept,\n\t\t\t// buffer to receive up front\n\t\t\tpfa.scratchBuffer.ptr,\n\t\t\t0,\n\t\t\t// size of local and remote addresses. normally + 16.\n\t\t\tsockaddr_in.sizeof + 16,\n\t\t\tsockaddr_in.sizeof + 16,\n\t\t\t&ignored, // bytes would be given through the iocp instead but im not even requesting the thing\n\t\t\t&overlapped\n\t\t);\n\n\t\treturn pfa;\n\t}\n\n\toverride void connect(Address to) { assert(0); }\n\n\tDWORD lastAnswer;\n\tubyte[1024] scratchBuffer;\n\tstatic assert(scratchBuffer.length > sockaddr_in.sizeof * 2 + 32);\n\n\tWSABUF[1] buffer;\n\tOVERLAPPED overlapped;\n\toverride ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) @trusted {\n\t\toverlapped = overlapped.init;\n\t\tbuffer[0].len = cast(DWORD) buf.length;\n\t\tbuffer[0].buf = cast(CHAR*) buf.ptr;\n\t\tfiber.setPostYield( () {\n\t\t\tif(!WSASend(handle, buffer.ptr, cast(DWORD) buffer.length, null, 0, &overlapped, null)) {\n\t\t\t\tif(GetLastError() != 997) {\n\t\t\t\t\t//throw new Exception(\"WSASend fail\");\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tFiber.yield();\n\t\treturn lastAnswer;\n\t}\n\toverride ptrdiff_t receive(scope void[] buf, SocketFlags flags) @trusted {\n\t\toverlapped = overlapped.init;\n\t\tbuffer[0].len = cast(DWORD) buf.length;\n\t\tbuffer[0].buf = cast(CHAR*) buf.ptr;\n\n\t\tDWORD flags2 = 0;\n\n\t\tfiber.setPostYield(() {\n\t\t\tif(!WSARecv(handle, buffer.ptr, cast(DWORD) buffer.length, null, &flags2 /* flags */, &overlapped, null)) {\n\t\t\t\tif(GetLastError() != 997) {\n\t\t\t\t\t//writeln(\"WSARecv \", WSAGetLastError());\n\t\t\t\t\t//throw new Exception(\"WSARecv fail\");\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tFiber.yield();\n\t\treturn lastAnswer;\n\t}\n\n\t// I might go back and implement these for udp things.\n\toverride ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags, ref Address from) @trusted {\n\t\tassert(0);\n\t}\n\toverride ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags) @trusted {\n\t\tassert(0);\n\t}\n\toverride ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags, Address to) @trusted {\n\t\tassert(0);\n\t}\n\toverride ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags) @trusted {\n\t\tassert(0);\n\t}\n\n\t// lol overload sets\n\talias send = typeof(super).send;\n\talias receive = typeof(super).receive;\n\talias sendTo = typeof(super).sendTo;\n\talias receiveFrom = typeof(super).receiveFrom;\n\n}\n}\n\nvoid doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) {\n\tassert(connection !is null);\n\tversion(cgi_use_fiber) {\n\t\tauto fiber = new CgiFiber(&doThreadHttpConnectionGuts!(CustomCgi, fun));\n\n\t\tversion(Windows) {\n\t\t\t(cast(PseudoblockingOverlappedSocket) connection).fiber = fiber;\n\t\t}\n\n\t\timport core.memory;\n\t\tGC.addRoot(cast(void*) fiber);\n\t\tfiber.connection = connection;\n\t\tfiber.proceed();\n\t} else {\n\t\tdoThreadHttpConnectionGuts!(CustomCgi, fun)(connection);\n\t}\n}\n\nvoid doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) {\n\tscope(failure) {\n\t\t// catch all for other errors\n\t\ttry {\n\t\t\tsendAll(connection, plainHttpError(false, \"500 Internal Server Error\", null));\n\t\t\tconnection.close();\n\t\t} catch(Exception e) {} // swallow it, we're aborting anyway.\n\t}\n\n\tbool closeConnection = alwaysCloseConnection;\n\n\t/+\n\tubyte[4096] inputBuffer = void;\n\tubyte[__traits(classInstanceSize, BufferedInputRange)] birBuffer = void;\n\tubyte[__traits(classInstanceSize, CustomCgi)] cgiBuffer = void;\n\n\tbirBuffer[] = cast(ubyte[]) typeid(BufferedInputRange).initializer()[];\n\tBufferedInputRange ir = cast(BufferedInputRange) cast(void*) birBuffer.ptr;\n\tir.__ctor(connection, inputBuffer[], true);\n\t+/\n\n\tauto ir = new BufferedInputRange(connection);\n\n\twhile(!ir.empty) {\n\n\t\tif(ir.view.length == 0) {\n\t\t\tir.popFront();\n\t\t\tif(ir.sourceClosed) {\n\t\t\t\tconnection.close();\n\t\t\t\tcloseConnection = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tCgi cgi;\n\t\ttry {\n\t\t\tcgi = new CustomCgi(ir, &closeConnection);\n\t\t\t// There's a bunch of these casts around because the type matches up with\n\t\t\t// the -version=.... specifiers, just you can also create a RequestServer\n\t\t\t// and instantiate the things where the types don't match up. It isn't exactly\n\t\t\t// correct but I also don't care rn. Might FIXME and either remove it later or something.\n\t\t\tcgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle;\n\t\t} catch(ConnectionClosedException ce) {\n\t\t\tcloseConnection = true;\n\t\t\tbreak;\n\t\t} catch(ConnectionException ce) {\n\t\t\t// broken pipe or something, just abort the connection\n\t\t\tcloseConnection = true;\n\t\t\tbreak;\n\t\t} catch(Throwable t) {\n\t\t\t// a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P\n\t\t\t// anyway let's kill the connection\n\t\t\tversion(CRuntime_Musl) {\n\t\t\t\tstderr.rawWrite(t.toString());\n\t\t\t\tstderr.rawWrite(\"\\n\");\n\t\t\t} else {\n\t\t\t\tstderr.writeln(t.toString());\n\t\t\t}\n\t\t\tsendAll(connection, plainHttpError(false, \"400 Bad Request\", t));\n\t\t\tcloseConnection = true;\n\t\t\tbreak;\n\t\t}\n\t\tassert(cgi !is null);\n\t\tscope(exit)\n\t\t\tcgi.dispose();\n\n\t\ttry {\n\t\t\tfun(cgi);\n\t\t\tcgi.close();\n\t\t\tif(cgi.websocketMode)\n\t\t\t\tcloseConnection = true;\n\t\t} catch(ConnectionException ce) {\n\t\t\t// broken pipe or something, just abort the connection\n\t\t\tcloseConnection = true;\n\t\t} catch(ConnectionClosedException ce) {\n\t\t\t// broken pipe or something, just abort the connection\n\t\t\tcloseConnection = true;\n\t\t} catch(Throwable t) {\n\t\t\t// a processing error can be recovered from\n\t\t\tversion(CRuntime_Musl) {} else\n\t\t\tstderr.writeln(t.toString);\n\t\t\tif(!handleException(cgi, t))\n\t\t\t\tcloseConnection = true;\n\t\t}\n\n\t\tif(globalStopFlag)\n\t\t\tcloseConnection = true;\n\n\t\tif(closeConnection || alwaysCloseConnection) {\n\t\t\tconnection.shutdown(SocketShutdown.BOTH);\n\t\t\tconnection.close();\n\t\t\tir.dispose();\n\t\t\tcloseConnection = false; // don't reclose after loop\n\t\t\tbreak;\n\t\t} else {\n\t\t\tif(ir.front.length) {\n\t\t\t\tir.popFront(); // we can't just discard the buffer, so get the next bit and keep chugging along\n\t\t\t} else if(ir.sourceClosed) {\n\t\t\t\tir.source.shutdown(SocketShutdown.BOTH);\n\t\t\t\tir.source.close();\n\t\t\t\tir.dispose();\n\t\t\t\tcloseConnection = false;\n\t\t\t} else {\n\t\t\t\tcontinue;\n\t\t\t\t// break; // this was for a keepalive experiment\n\t\t\t}\n\t\t}\n\t}\n\n\tif(closeConnection) {\n\t\tconnection.shutdown(SocketShutdown.BOTH);\n\t\tconnection.close();\n\t\tir.dispose();\n\t}\n\n\t// I am otherwise NOT closing it here because the parent thread might still be able to make use of the keep-alive connection!\n}\n\nvoid doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket connection) {\n\t// and now we can buffer\n\tscope(failure)\n\t\tconnection.close();\n\n\timport al = std.algorithm;\n\n\tsize_t size;\n\n\tstring[string] headers;\n\n\tauto range = new BufferedInputRange(connection);\n\tmore_data:\n\tauto chunk = range.front();\n\t// waiting for colon for header length\n\tauto idx = indexOf(cast(string) chunk, ':');\n\tif(idx == -1) {\n\t\ttry {\n\t\t\trange.popFront();\n\t\t} catch(Exception e) {\n\t\t\t// it is just closed, no big deal\n\t\t\tconnection.close();\n\t\t\treturn;\n\t\t}\n\t\tgoto more_data;\n\t}\n\n\tsize = to!size_t(cast(string) chunk[0 .. idx]);\n\tchunk = range.consume(idx + 1);\n\t// reading headers\n\tif(chunk.length < size)\n\t\trange.popFront(0, size + 1);\n\t// we are now guaranteed to have enough\n\tchunk = range.front();\n\tassert(chunk.length > size);\n\n\tidx = 0;\n\tstring key;\n\tstring value;\n\tforeach(part; al.splitter(chunk, '\\0')) {\n\t\tif(idx & 1) { // odd is value\n\t\t\tvalue = cast(string)(part.idup);\n\t\t\theaders[key] = value; // commit\n\t\t} else\n\t\t\tkey = cast(string)(part.idup);\n\t\tidx++;\n\t}\n\n\tenforce(chunk[size] == ','); // the terminator\n\n\trange.consume(size + 1);\n\t// reading data\n\t// this will be done by Cgi\n\n\tconst(ubyte)[] getScgiChunk() {\n\t\t// we are already primed\n\t\tauto data = range.front();\n\t\tif(data.length == 0 && !range.sourceClosed) {\n\t\t\trange.popFront(0);\n\t\t\tdata = range.front();\n\t\t} else if (range.sourceClosed)\n\t\t\trange.source.close();\n\n\t\treturn data;\n\t}\n\n\tvoid writeScgi(const(ubyte)[] data) {\n\t\tsendAll(connection, data);\n\t}\n\n\tvoid flushScgi() {\n\t\t// I don't *think* I have to do anything....\n\t}\n\n\tCgi cgi;\n\ttry {\n\t\tcgi = new CustomCgi(maxContentLength, headers, &getScgiChunk, &writeScgi, &flushScgi);\n\t\tcgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle;\n\t} catch(Throwable t) {\n\t\tsendAll(connection, plainHttpError(true, \"400 Bad Request\", t));\n\t\tconnection.close();\n\t\treturn; // this connection is dead\n\t}\n\tassert(cgi !is null);\n\tscope(exit) cgi.dispose();\n\ttry {\n\t\tfun(cgi);\n\t\tcgi.close();\n\t\tconnection.close();\n\t} catch(Throwable t) {\n\t\t// no std err\n\t\tif(!handleException(cgi, t)) {\n\t\t\tconnection.close();\n\t\t\treturn;\n\t\t} else {\n\t\t\tconnection.close();\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nstring printDate(DateTime date) {\n\tchar[29] buffer = void;\n\tprintDateToBuffer(date, buffer[]);\n\treturn buffer.idup;\n}\n\nint printDateToBuffer(DateTime date, char[] buffer) @nogc {\n\tassert(buffer.length >= 29);\n\t// 29 static length ?\n\n\tstatic immutable daysOfWeek = [\n\t\t\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"\n\t];\n\n\tstatic immutable months = [\n\t\tnull, \"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"\n\t];\n\n\tbuffer[0 .. 3] = daysOfWeek[date.dayOfWeek];\n\tbuffer[3 .. 5] = \", \";\n\tbuffer[5] = date.day / 10 + '0';\n\tbuffer[6] = date.day % 10 + '0';\n\tbuffer[7] = ' ';\n\tbuffer[8 .. 11] = months[date.month];\n\tbuffer[11] = ' ';\n\tauto y = date.year;\n\tbuffer[12] = cast(char) (y / 1000 + '0'); y %= 1000;\n\tbuffer[13] = cast(char) (y / 100 + '0'); y %= 100;\n\tbuffer[14] = cast(char) (y / 10 + '0'); y %= 10;\n\tbuffer[15] = cast(char) (y + '0');\n\tbuffer[16] = ' ';\n\tbuffer[17] = date.hour / 10 + '0';\n\tbuffer[18] = date.hour % 10 + '0';\n\tbuffer[19] = ':';\n\tbuffer[20] = date.minute / 10 + '0';\n\tbuffer[21] = date.minute % 10 + '0';\n\tbuffer[22] = ':';\n\tbuffer[23] = date.second / 10 + '0';\n\tbuffer[24] = date.second % 10 + '0';\n\tbuffer[25 .. $] = \" GMT\";\n\n\treturn 29;\n}\n\n\n// Referencing this gigantic typeid seems to remind the compiler\n// to actually put the symbol in the object file. I guess the immutable\n// assoc array array isn't actually included in druntime\nvoid hackAroundLinkerError() {\n      stdout.rawWrite(typeid(const(immutable(char)[][])[immutable(char)[]]).toString());\n      stdout.rawWrite(typeid(immutable(char)[][][immutable(char)[]]).toString());\n      stdout.rawWrite(typeid(Cgi.UploadedFile[immutable(char)[]]).toString());\n      stdout.rawWrite(typeid(Cgi.UploadedFile[][immutable(char)[]]).toString());\n      stdout.rawWrite(typeid(immutable(Cgi.UploadedFile)[immutable(char)[]]).toString());\n      stdout.rawWrite(typeid(immutable(Cgi.UploadedFile[])[immutable(char)[]]).toString());\n      stdout.rawWrite(typeid(immutable(char[])[immutable(char)[]]).toString());\n      // this is getting kinda ridiculous btw. Moving assoc arrays\n      // to the library is the pain that keeps on coming.\n\n      // eh this broke the build on the work server\n      // stdout.rawWrite(typeid(immutable(char)[][immutable(string[])]));\n      stdout.rawWrite(typeid(immutable(string[])[immutable(char)[]]).toString());\n}\n\n\n\n\n\nversion(fastcgi) {\n\tpragma(lib, \"fcgi\");\n\n\tstatic if(size_t.sizeof == 8) // 64 bit\n\t\talias long c_int;\n\telse\n\t\talias int c_int;\n\n\textern(C) {\n\t\tstruct FCGX_Stream {\n\t\t\tubyte* rdNext;\n\t\t\tubyte* wrNext;\n\t\t\tubyte* stop;\n\t\t\tubyte* stopUnget;\n\t\t\tc_int isReader;\n\t\t\tc_int isClosed;\n\t\t\tc_int wasFCloseCalled;\n\t\t\tc_int FCGI_errno;\n\t\t\tvoid* function(FCGX_Stream* stream) fillBuffProc;\n\t\t\tvoid* function(FCGX_Stream* stream, c_int doClose) emptyBuffProc;\n\t\t\tvoid* data;\n\t\t}\n\n\t\t// note: this is meant to be opaque, so don't access it directly\n\t\tstruct FCGX_Request {\n\t\t\tint requestId;\n\t\t\tint role;\n\t\t\tFCGX_Stream* inStream;\n\t\t\tFCGX_Stream* outStream;\n\t\t\tFCGX_Stream* errStream;\n\t\t\tchar** envp;\n\t\t\tvoid* paramsPtr;\n\t\t\tint ipcFd;\n\t\t\tint isBeginProcessed;\n\t\t\tint keepConnection;\n\t\t\tint appStatus;\n\t\t\tint nWriters;\n\t\t\tint flags;\n\t\t\tint listen_sock;\n\t\t}\n\n\t\tint FCGX_InitRequest(FCGX_Request *request, int sock, int flags);\n\t\tvoid FCGX_Init();\n\n\t\tint FCGX_Accept_r(FCGX_Request *request);\n\n\n\t\talias char** FCGX_ParamArray;\n\n\t\tc_int FCGX_Accept(FCGX_Stream** stdin, FCGX_Stream** stdout, FCGX_Stream** stderr, FCGX_ParamArray* envp);\n\t\tc_int FCGX_GetChar(FCGX_Stream* stream);\n\t\tc_int FCGX_PutStr(const ubyte* str, c_int n, FCGX_Stream* stream);\n\t\tint FCGX_HasSeenEOF(FCGX_Stream* stream);\n\t\tc_int FCGX_FFlush(FCGX_Stream *stream);\n\n\t\tint FCGX_OpenSocket(in char*, int);\n\t}\n}\n\n\n/* This might go int a separate module eventually. It is a network input helper class. */\n\nimport std.socket;\n\nversion(cgi_use_fiber) {\n\timport core.thread;\n\n\tversion(linux) {\n\t\timport core.sys.linux.epoll;\n\n\t\tint epfd = -1; // thread local because EPOLLEXCLUSIVE works much better this way... weirdly.\n\t} else version(Windows) {\n\t\t// declaring the iocp thing below...\n\t} else static assert(0, \"The hybrid fiber server is not implemented on your OS.\");\n}\n\nversion(Windows)\n\t__gshared HANDLE iocp;\n\nversion(cgi_use_fiber) {\n\tversion(linux)\n\tprivate enum WakeupEvent {\n\t\tRead = EPOLLIN,\n\t\tWrite = EPOLLOUT\n\t}\n\telse version(Windows)\n\tprivate enum WakeupEvent {\n\t\tRead, Write\n\t}\n\telse static assert(0);\n}\n\nversion(cgi_use_fiber)\nprivate void registerEventWakeup(bool* registered, Socket source, WakeupEvent e) @nogc {\n\n\t// static cast since I know what i have in here and don't want to pay for dynamic cast\n\tauto f = cast(CgiFiber) cast(void*) Fiber.getThis();\n\n\tversion(linux) {\n\t\tf.setPostYield = () {\n\t\t\tif(*registered) {\n\t\t\t\t// rearm\n\t\t\t\tepoll_event evt;\n\t\t\t\tevt.events = e | EPOLLONESHOT;\n\t\t\t\tevt.data.ptr = cast(void*) f;\n\t\t\t\tif(epoll_ctl(epfd, EPOLL_CTL_MOD, source.handle, &evt) == -1)\n\t\t\t\t\tthrow new Exception(\"epoll_ctl\");\n\t\t\t} else {\n\t\t\t\t// initial registration\n\t\t\t\t*registered = true ;\n\t\t\t\tint fd = source.handle;\n\t\t\t\tepoll_event evt;\n\t\t\t\tevt.events = e | EPOLLONESHOT;\n\t\t\t\tevt.data.ptr = cast(void*) f;\n\t\t\t\tif(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt) == -1)\n\t\t\t\t\tthrow new Exception(\"epoll_ctl\");\n\t\t\t}\n\t\t};\n\n\t\tFiber.yield();\n\n\t\tf.setPostYield(null);\n\t} else version(Windows) {\n\t\tFiber.yield();\n\t}\n\telse static assert(0);\n}\n\nversion(cgi_use_fiber)\nvoid unregisterSource(Socket s) {\n\tversion(linux) {\n\t\tepoll_event evt;\n\t\tepoll_ctl(epfd, EPOLL_CTL_DEL, s.handle(), &evt);\n\t} else version(Windows) {\n\t\t// intentionally blank\n\t}\n\telse static assert(0);\n}\n\n// it is a class primarily for reference semantics\n// I might change this interface\n/// This is NOT ACTUALLY an input range! It is too different. Historical mistake kinda.\nclass BufferedInputRange {\n\tversion(Posix)\n\tthis(int source, ubyte[] buffer = null) {\n\t\tthis(new Socket(cast(socket_t) source, AddressFamily.INET), buffer);\n\t}\n\n\tthis(Socket source, ubyte[] buffer = null, bool allowGrowth = true) {\n\t\t// if they connect but never send stuff to us, we don't want it wasting the process\n\t\t// so setting a time out\n\t\tversion(cgi_use_fiber)\n\t\t\tsource.blocking = false;\n\t\telse\n\t\t\tsource.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!\"seconds\"(3));\n\n\t\tthis.source = source;\n\t\tif(buffer is null) {\n\t\t\tunderlyingBuffer = new ubyte[4096];\n\t\t\tthis.allowGrowth = true;\n\t\t} else {\n\t\t\tunderlyingBuffer = buffer;\n\t\t\tthis.allowGrowth = allowGrowth;\n\t\t}\n\n\t\tassert(underlyingBuffer.length);\n\n\t\t// we assume view.ptr is always inside underlyingBuffer\n\t\tview = underlyingBuffer[0 .. 0];\n\n\t\tpopFront(); // prime\n\t}\n\n\tversion(cgi_use_fiber) {\n\t\tbool registered;\n\t}\n\n\tvoid dispose() {\n\t\tversion(cgi_use_fiber) {\n\t\t\tif(registered)\n\t\t\t\tunregisterSource(source);\n\t\t}\n\t}\n\n\t/**\n\t\tA slight difference from regular ranges is you can give it the maximum\n\t\tnumber of bytes to consume.\n\n\t\tIMPORTANT NOTE: the default is to consume nothing, so if you don't call\n\t\tconsume() yourself and use a regular foreach, it will infinitely loop!\n\n\t\tThe default is to do what a normal range does, and consume the whole buffer\n\t\tand wait for additional input.\n\n\t\tYou can also specify 0, to append to the buffer, or any other number\n\t\tto remove the front n bytes and wait for more.\n\t*/\n\tvoid popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) {\n\t\tif(sourceClosed)\n\t\t\tthrow new ConnectionClosedException(\"can't get any more data from a closed source\");\n\t\tif(!skipConsume)\n\t\t\tconsume(maxBytesToConsume);\n\n\t\t// we might have to grow the buffer\n\t\tif(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) {\n\t\t\tif(allowGrowth) {\n\t\t\t//import std.stdio; writeln(\"growth\");\n\t\t\t\tauto viewStart = view.ptr - underlyingBuffer.ptr;\n\t\t\t\tsize_t growth = 4096;\n\t\t\t\t// make sure we have enough for what we're being asked for\n\t\t\t\tif(minBytesToSettleFor > 0 && minBytesToSettleFor - underlyingBuffer.length > growth)\n\t\t\t\t\tgrowth = minBytesToSettleFor - underlyingBuffer.length;\n\t\t\t\t//import std.stdio; writeln(underlyingBuffer.length, \" \", viewStart, \" \", view.length, \" \", growth,  \" \", minBytesToSettleFor, \" \", minBytesToSettleFor - underlyingBuffer.length);\n\t\t\t\tunderlyingBuffer.length += growth;\n\t\t\t\tview = underlyingBuffer[viewStart .. view.length];\n\t\t\t} else\n\t\t\t\tthrow new Exception(\"No room left in the buffer\");\n\t\t}\n\n\t\tdo {\n\t\t\tauto freeSpace = underlyingBuffer[view.ptr - underlyingBuffer.ptr + view.length .. $];\n\t\t\ttry_again:\n\t\t\tauto ret = source.receive(freeSpace);\n\t\t\tif(ret == Socket.ERROR) {\n\t\t\t\tif(wouldHaveBlocked()) {\n\t\t\t\t\tversion(cgi_use_fiber) {\n\t\t\t\t\t\tregisterEventWakeup(&registered, source, WakeupEvent.Read);\n\t\t\t\t\t\tgoto try_again;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// gonna treat a timeout here as a close\n\t\t\t\t\t\tsourceClosed = true;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tversion(Posix) {\n\t\t\t\t\timport core.stdc.errno;\n\t\t\t\t\tif(errno == EINTR || errno == EAGAIN) {\n\t\t\t\t\t\tgoto try_again;\n\t\t\t\t\t}\n\t\t\t\t\tif(errno == ECONNRESET) {\n\t\t\t\t\t\tsourceClosed = true;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthrow new Exception(lastSocketError); // FIXME\n\t\t\t}\n\t\t\tif(ret == 0) {\n\t\t\t\tsourceClosed = true;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t//import std.stdio; writeln(view.ptr); writeln(underlyingBuffer.ptr); writeln(view.length, \" \", ret, \" = \", view.length + ret);\n\t\t\tview = underlyingBuffer[view.ptr - underlyingBuffer.ptr .. view.length + ret];\n\t\t\t//import std.stdio; writeln(cast(string) view);\n\t\t} while(view.length < minBytesToSettleFor);\n\t}\n\n\t/// Removes n bytes from the front of the buffer, and returns the new buffer slice.\n\t/// You might want to idup the data you are consuming if you store it, since it may\n\t/// be overwritten on the new popFront.\n\t///\n\t/// You do not need to call this if you always want to wait for more data when you\n\t/// consume some.\n\tubyte[] consume(size_t bytes) {\n\t\tview = view[bytes > $ ? $ : bytes .. $];\n\t\tif(view.length == 0) {\n\t\t\tview = underlyingBuffer[0 .. 0]; // go ahead and reuse the beginning\n\t\t\t/*\n\t\t\twriteln(\"HERE\");\n\t\t\tpopFront(0, 0, true); // try to load more if we can, checks if the source is closed\n\t\t\twriteln(cast(string)front);\n\t\t\twriteln(\"DONE\");\n\t\t\t*/\n\t\t}\n\t\treturn front;\n\t}\n\n\tbool empty() {\n\t\treturn sourceClosed && view.length == 0;\n\t}\n\n\tubyte[] front() {\n\t\treturn view;\n\t}\n\n\tinvariant() {\n\t\tassert(view.ptr >= underlyingBuffer.ptr);\n\t\t// it should never be equal, since if that happens view ought to be empty, and thus reusing the buffer\n\t\tassert(view.ptr < underlyingBuffer.ptr + underlyingBuffer.length);\n\t}\n\n\tubyte[] underlyingBuffer;\n\tbool allowGrowth;\n\tubyte[] view;\n\tSocket source;\n\tbool sourceClosed;\n}\n\nprivate class FakeSocketForStdin : Socket {\n\timport std.stdio;\n\n\tthis() {\n\n\t}\n\n\tprivate bool closed;\n\n\toverride ptrdiff_t receive(scope void[] buffer, std.socket.SocketFlags) @trusted {\n\t\tif(closed)\n\t\t\tthrow new Exception(\"Closed\");\n\t\treturn stdin.rawRead(buffer).length;\n\t}\n\n\toverride ptrdiff_t send(const scope void[] buffer, std.socket.SocketFlags) @trusted {\n\t\tif(closed)\n\t\t\tthrow new Exception(\"Closed\");\n\t\tstdout.rawWrite(buffer);\n\t\treturn buffer.length;\n\t}\n\n\toverride void close() @trusted scope {\n\t\t(cast(void delegate() @nogc nothrow) &realClose)();\n\t}\n\n\toverride void shutdown(SocketShutdown s) {\n\t\t// FIXME\n\t}\n\n\toverride void setOption(SocketOptionLevel, SocketOption, scope void[]) {}\n\toverride void setOption(SocketOptionLevel, SocketOption, Duration) {}\n\n\toverride @property @trusted Address remoteAddress() { return null; }\n\toverride @property @trusted Address localAddress() { return null; }\n\n\tvoid realClose() {\n\t\tclosed = true;\n\t\ttry {\n\t\t\tstdin.close();\n\t\t\tstdout.close();\n\t\t} catch(Exception e) {\n\n\t\t}\n\t}\n}\n\nimport core.sync.semaphore;\nimport core.atomic;\n\n/**\n\tTo use this thing:\n\n\t---\n\tvoid handler(Socket s) { do something... }\n\tauto manager = new ListeningConnectionManager(\"127.0.0.1\", 80, &handler, &delegateThatDropsPrivileges);\n\tmanager.listen();\n\t---\n\n\tThe 4th parameter is optional.\n\n\tI suggest you use BufferedInputRange(connection) to handle the input. As a packet\n\tcomes in, you will get control. You can just continue; though to fetch more.\n\n\n\tFIXME: should I offer an event based async thing like netman did too? Yeah, probably.\n*/\nclass ListeningConnectionManager {\n\tSemaphore semaphore;\n\tSocket[256] queue;\n\tshared(ubyte) nextIndexFront;\n\tubyte nextIndexBack;\n\tshared(int) queueLength;\n\n\tSocket acceptCancelable() {\n\t\tversion(Posix) {\n\t\t\timport core.sys.posix.sys.select;\n\t\t\tfd_set read_fds;\n\t\t\tFD_ZERO(&read_fds);\n\t\t\tFD_SET(listener.handle, &read_fds);\n\t\t\tif(cancelfd != -1)\n\t\t\t\tFD_SET(cancelfd, &read_fds);\n\t\t\tauto max = listener.handle > cancelfd ? listener.handle : cancelfd;\n\t\t\tauto ret = select(max + 1, &read_fds, null, null, null);\n\t\t\tif(ret == -1) {\n\t\t\t\timport core.stdc.errno;\n\t\t\t\tif(errno == EINTR)\n\t\t\t\t\treturn null;\n\t\t\t\telse\n\t\t\t\t\tthrow new Exception(\"wtf select\");\n\t\t\t}\n\n\t\t\tif(cancelfd != -1 && FD_ISSET(cancelfd, &read_fds)) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif(FD_ISSET(listener.handle, &read_fds))\n\t\t\t\treturn listener.accept();\n\n\t\t\treturn null;\n\t\t} else {\n\n\t\t\tSocket socket = listener;\n\n\t\t\tauto check = new SocketSet();\n\n\t\t\tkeep_looping:\n\t\t\tcheck.reset();\n\t\t\tcheck.add(socket);\n\n\t\t\t// just to check the stop flag on a kinda busy loop. i hate this FIXME\n\t\t\tauto got = Socket.select(check, null, null, 3.seconds);\n\t\t\tif(got > 0)\n\t\t\t\treturn listener.accept();\n\t\t\tif(globalStopFlag)\n\t\t\t\treturn null;\n\t\t\telse\n\t\t\t\tgoto keep_looping;\n\t\t}\n\t}\n\n\tint defaultNumberOfThreads() {\n\t\timport std.parallelism;\n\t\tversion(cgi_use_fiber) {\n\t\t\treturn totalCPUs * 1 + 1;\n\t\t} else {\n\t\t\t// I times 4 here because there's a good chance some will be blocked on i/o.\n\t\t\treturn totalCPUs * 4;\n\t\t}\n\n\t}\n\n\tvoid listen() {\n\t\tshared(int) loopBroken;\n\n\t\tversion(Posix) {\n\t\t\timport core.sys.posix.signal;\n\t\t\tsignal(SIGPIPE, SIG_IGN);\n\t\t}\n\n\t\tversion(linux) {\n\t\t\tif(cancelfd == -1)\n\t\t\t\tcancelfd = eventfd(0, 0);\n\t\t}\n\n\t\tversion(cgi_no_threads) {\n\t\t\t// NEVER USE THIS\n\t\t\t// it exists only for debugging and other special occasions\n\n\t\t\t// the thread mode is faster and less likely to stall the whole\n\t\t\t// thing when a request is slow\n\t\t\twhile(!loopBroken && !globalStopFlag) {\n\t\t\t\tauto sn = acceptCancelable();\n\t\t\t\tif(sn is null) continue;\n\t\t\t\tcloexec(sn);\n\t\t\t\ttry {\n\t\t\t\t\thandler(sn);\n\t\t\t\t} catch(Exception e) {\n\t\t\t\t\t// 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)\n\t\t\t\t\tsn.close();\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\n\t\t\tif(useFork) {\n\t\t\t\tversion(linux) {\n\t\t\t\t\t//asm { int 3; }\n\t\t\t\t\tfork();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tversion(cgi_use_fiber) {\n\n\t\t\t\tversion(Windows) {\n\t\t\t\t\tlistener.accept();\n\t\t\t\t}\n\n\t\t\t\tWorkerThread[] threads = new WorkerThread[](numberOfThreads);\n\t\t\t\tforeach(i, ref thread; threads) {\n\t\t\t\t\tthread = new WorkerThread(this, handler, cast(int) i);\n\t\t\t\t\tthread.start();\n\t\t\t\t}\n\n\t\t\t\tbool fiber_crash_check() {\n\t\t\t\t\tbool hasAnyRunning;\n\t\t\t\t\tforeach(thread; threads) {\n\t\t\t\t\t\tif(!thread.isRunning) {\n\t\t\t\t\t\t\tthread.join();\n\t\t\t\t\t\t} else hasAnyRunning = true;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn (!hasAnyRunning);\n\t\t\t\t}\n\n\n\t\t\t\twhile(!globalStopFlag) {\n\t\t\t\t\tThread.sleep(1.seconds);\n\t\t\t\t\tif(fiber_crash_check())\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\tsemaphore = new Semaphore();\n\n\t\t\t\tConnectionThread[] threads = new ConnectionThread[](numberOfThreads);\n\t\t\t\tforeach(i, ref thread; threads) {\n\t\t\t\t\tthread = new ConnectionThread(this, handler, cast(int) i);\n\t\t\t\t\tthread.start();\n\t\t\t\t}\n\n\t\t\t\twhile(!loopBroken && !globalStopFlag) {\n\t\t\t\t\tSocket sn;\n\n\t\t\t\t\tbool crash_check() {\n\t\t\t\t\t\tbool hasAnyRunning;\n\t\t\t\t\t\tforeach(thread; threads) {\n\t\t\t\t\t\t\tif(!thread.isRunning) {\n\t\t\t\t\t\t\t\tthread.join();\n\t\t\t\t\t\t\t} else hasAnyRunning = true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn (!hasAnyRunning);\n\t\t\t\t\t}\n\n\n\t\t\t\t\tvoid accept_new_connection() {\n\t\t\t\t\t\tsn = acceptCancelable();\n\t\t\t\t\t\tif(sn is null) return;\n\t\t\t\t\t\tcloexec(sn);\n\t\t\t\t\t\tif(tcp) {\n\t\t\t\t\t\t\t// disable Nagle's algorithm to avoid a 40ms delay when we send/recv\n\t\t\t\t\t\t\t// on the socket because we do some buffering internally. I think this helps,\n\t\t\t\t\t\t\t// certainly does for small requests, and I think it does for larger ones too\n\t\t\t\t\t\t\tsn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1);\n\n\t\t\t\t\t\t\tsn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!\"seconds\"(10));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tvoid existing_connection_new_data() {\n\t\t\t\t\t\t// wait until a slot opens up\n\t\t\t\t\t\t//int waited = 0;\n\t\t\t\t\t\twhile(queueLength >= queue.length) {\n\t\t\t\t\t\t\tThread.sleep(1.msecs);\n\t\t\t\t\t\t\t//waited ++;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t//if(waited) {import std.stdio; writeln(waited);}\n\t\t\t\t\t\tsynchronized(this) {\n\t\t\t\t\t\t\tqueue[nextIndexBack] = sn;\n\t\t\t\t\t\t\tnextIndexBack++;\n\t\t\t\t\t\t\tatomicOp!\"+=\"(queueLength, 1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsemaphore.notify();\n\t\t\t\t\t}\n\n\n\t\t\t\t\taccept_new_connection();\n\t\t\t\t\tif(sn !is null)\n\t\t\t\t\t\texisting_connection_new_data();\n\t\t\t\t\telse if(sn is null && globalStopFlag) {\n\t\t\t\t\t\tforeach(thread; threads) {\n\t\t\t\t\t\t\tsemaphore.notify();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tThread.sleep(50.msecs);\n\t\t\t\t\t}\n\n\t\t\t\t\tif(crash_check())\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// FIXME: i typically stop this with ctrl+c which never\n\t\t\t// actually gets here. i need to do a sigint handler.\n\t\t\tif(cleanup)\n\t\t\t\tcleanup();\n\t\t}\n\t}\n\n\t//version(linux)\n\t\t//int epoll_fd;\n\n\tbool tcp;\n\tvoid delegate() cleanup;\n\n\tprivate void function(Socket) fhandler;\n\tprivate void dg_handler(Socket s) {\n\t\tfhandler(s);\n\t}\n\tthis(string host, ushort port, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) {\n\t\tfhandler = handler;\n\t\tthis(host, port, &dg_handler, dropPrivs, useFork, numberOfThreads);\n\t}\n\n\tthis(string host, ushort port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) {\n\t\tthis.handler = handler;\n\t\tthis.useFork = useFork;\n\t\tthis.numberOfThreads = numberOfThreads ? numberOfThreads : defaultNumberOfThreads();\n\n\t\tlistener = startListening(host, port, tcp, cleanup, 128, dropPrivs);\n\n\t\tversion(cgi_use_fiber)\n\t\tif(useFork)\n\t\t\tlistener.blocking = false;\n\n\t\t// this is the UI control thread and thus gets more priority\n\t\tThread.getThis.priority = Thread.PRIORITY_MAX;\n\t}\n\n\tSocket listener;\n\tvoid delegate(Socket) handler;\n\n\timmutable bool useFork;\n\tint numberOfThreads;\n}\n\nSocket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue, void delegate() dropPrivs) {\n\tSocket listener;\n\tif(host.startsWith(\"unix:\")) {\n\t\tversion(Posix) {\n\t\t\tlistener = new Socket(AddressFamily.UNIX, SocketType.STREAM);\n\t\t\tcloexec(listener);\n\t\t\tstring filename = host[\"unix:\".length .. $].idup;\n\t\t\tlistener.bind(new UnixAddress(filename));\n\t\t\tcleanup = delegate() {\n\t\t\t\tlistener.close();\n\t\t\t\timport std.file;\n\t\t\t\tremove(filename);\n\t\t\t};\n\t\t\ttcp = false;\n\t\t} else {\n\t\t\tthrow new Exception(\"unix sockets not supported on this system\");\n\t\t}\n\t} else if(host.startsWith(\"abstract:\")) {\n\t\tversion(linux) {\n\t\t\tlistener = new Socket(AddressFamily.UNIX, SocketType.STREAM);\n\t\t\tcloexec(listener);\n\t\t\tstring filename = \"\\0\" ~ host[\"abstract:\".length .. $];\n\t\t\timport std.stdio; stderr.writeln(\"Listening to abstract unix domain socket: \", host[\"abstract:\".length .. $]);\n\t\t\tlistener.bind(new UnixAddress(filename));\n\t\t\ttcp = false;\n\t\t} else {\n\t\t\tthrow new Exception(\"abstract unix sockets not supported on this system\");\n\t\t}\n\t} else {\n\t\tversion(cgi_use_fiber) {\n\t\t\tversion(Windows)\n\t\t\t\tlistener = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM);\n\t\t\telse\n\t\t\t\tlistener = new TcpSocket();\n\t\t} else {\n\t\t\tlistener = new TcpSocket();\n\t\t}\n\t\tcloexec(listener);\n\t\tlistener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);\n\t\tlistener.bind(host.length ? parseAddress(host, port) : new InternetAddress(port));\n\t\tcleanup = delegate() {\n\t\t\tlistener.close();\n\t\t};\n\t\ttcp = true;\n\t}\n\n\tlistener.listen(backQueue);\n\n\tif (dropPrivs !is null) // can be null, backwards compatibility\n\t\tdropPrivs();\n\n\treturn listener;\n}\n\n// 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.\nvoid sendAll(Socket s, const(void)[] data, string file = __FILE__, size_t line = __LINE__) {\n\tif(data.length == 0) return;\n\tptrdiff_t amount;\n\t//import std.stdio; writeln(\"***\",cast(string) data,\"///\");\n\tdo {\n\t\tamount = s.send(data);\n\t\tif(amount == Socket.ERROR) {\n\t\t\tversion(cgi_use_fiber) {\n\t\t\t\tif(wouldHaveBlocked()) {\n\t\t\t\t\tbool registered = true;\n\t\t\t\t\tregisterEventWakeup(&registered, s, WakeupEvent.Write);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new ConnectionException(s, lastSocketError, file, line);\n\t\t}\n\t\tassert(amount > 0);\n\n\t\tdata = data[amount .. $];\n\t} while(data.length);\n}\n\nclass ConnectionException : Exception {\n\tSocket socket;\n\tthis(Socket s, string msg, string file = __FILE__, size_t line = __LINE__) {\n\t\tthis.socket = s;\n\t\tsuper(msg, file, line);\n\t}\n}\n\nalias void delegate(Socket) CMT;\n\nimport core.thread;\n/+\n\tcgi.d now uses a hybrid of event i/o and threads at the top level.\n\n\tTop level thread is responsible for accepting sockets and selecting on them.\n\n\tIt then indicates to a child that a request is pending, and any random worker\n\tthread that is free handles it. It goes into blocking mode and handles that\n\thttp request to completion.\n\n\tAt that point, it goes back into the waiting queue.\n\n\n\tThis concept is only implemented on Linux. On all other systems, it still\n\tuses the worker threads and semaphores (which is perfectly fine for a lot of\n\tthings! Just having a great number of keep-alive connections will break that.)\n\n\n\tSo the algorithm is:\n\n\tselect(accept, event, pending)\n\t\tif accept -> send socket to free thread, if any. if not, add socket to queue\n\t\tif event -> send the signaling thread a socket from the queue, if not, mark it free\n\t\t\t- event might block until it can be *written* to. it is a fifo sending socket fds!\n\n\tA worker only does one http request at a time, then signals its availability back to the boss.\n\n\tThe socket the worker was just doing should be added to the one-off epoll read. If it is closed,\n\tgreat, we can get rid of it. Otherwise, it is considered `pending`. The *kernel* manages that; the\n\tactual FD will not be kept out here.\n\n\tSo:\n\t\tqueue = sockets we know are ready to read now, but no worker thread is available\n\t\tidle list = worker threads not doing anything else. they signal back and forth\n\n\tthe workers all read off the event fd. This is the semaphore wait\n\n\tthe boss waits on accept or other sockets read events (one off! and level triggered). If anything happens wrt ready read,\n\tit puts it in the queue and writes to the event fd.\n\n\tThe child could put the socket back in the epoll thing itself.\n\n\tThe child needs to be able to gracefully handle being given a socket that just closed with no work.\n+/\nclass ConnectionThread : Thread {\n\tthis(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) {\n\t\tthis.lcm = lcm;\n\t\tthis.dg = dg;\n\t\tthis.myThreadNumber = myThreadNumber;\n\t\tsuper(&run);\n\t}\n\n\tvoid run() {\n\t\twhile(true) {\n\t\t\t// so if there's a bunch of idle keep-alive connections, it can\n\t\t\t// consume all the worker threads... just sitting there.\n\t\t\tlcm.semaphore.wait();\n\t\t\tif(globalStopFlag)\n\t\t\t\treturn;\n\t\t\tSocket socket;\n\t\t\tsynchronized(lcm) {\n\t\t\t\tauto idx = lcm.nextIndexFront;\n\t\t\t\tsocket = lcm.queue[idx];\n\t\t\t\tlcm.queue[idx] = null;\n\t\t\t\tatomicOp!\"+=\"(lcm.nextIndexFront, 1);\n\t\t\t\tatomicOp!\"-=\"(lcm.queueLength, 1);\n\t\t\t}\n\t\t\ttry {\n\t\t\t//import std.stdio; writeln(myThreadNumber, \" taking it\");\n\t\t\t\tdg(socket);\n\t\t\t\t/+\n\t\t\t\tif(socket.isAlive) {\n\t\t\t\t\t// process it more later\n\t\t\t\t\tversion(linux) {\n\t\t\t\t\t\timport core.sys.linux.epoll;\n\t\t\t\t\t\tepoll_event ev;\n\t\t\t\t\t\tev.events = EPOLLIN | EPOLLONESHOT | EPOLLET;\n\t\t\t\t\t\tev.data.fd = socket.handle;\n\t\t\t\t\t\timport std.stdio; writeln(\"adding\");\n\t\t\t\t\t\tif(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_ADD, socket.handle, &ev) == -1) {\n\t\t\t\t\t\t\tif(errno == EEXIST) {\n\t\t\t\t\t\t\t\tev.events = EPOLLIN | EPOLLONESHOT | EPOLLET;\n\t\t\t\t\t\t\t\tev.data.fd = socket.handle;\n\t\t\t\t\t\t\t\tif(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_MOD, socket.handle, &ev) == -1)\n\t\t\t\t\t\t\t\t\tthrow new Exception(\"epoll_ctl \" ~ to!string(errno));\n\t\t\t\t\t\t\t} else\n\t\t\t\t\t\t\t\tthrow new Exception(\"epoll_ctl \" ~ to!string(errno));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t//import std.stdio; writeln(\"keep alive\");\n\t\t\t\t\t\t// writing to this private member is to prevent the GC from closing my precious socket when I'm trying to use it later\n\t\t\t\t\t\t__traits(getMember, socket, \"sock\") = cast(socket_t) -1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontinue; // hope it times out in a reasonable amount of time...\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t+/\n\t\t\t} catch(ConnectionClosedException e) {\n\t\t\t\t// can just ignore this, it is fairly normal\n\t\t\t\tsocket.close();\n\t\t\t} catch(Throwable e) {\n\t\t\t\timport std.stdio; stderr.rawWrite(e.toString); stderr.rawWrite(\"\\n\");\n\t\t\t\tsocket.close();\n\t\t\t}\n\t\t}\n\t}\n\n\tListeningConnectionManager lcm;\n\tCMT dg;\n\tint myThreadNumber;\n}\n\nversion(cgi_use_fiber)\nclass WorkerThread : Thread {\n\tthis(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) {\n\t\tthis.lcm = lcm;\n\t\tthis.dg = dg;\n\t\tthis.myThreadNumber = myThreadNumber;\n\t\tsuper(&run);\n\t}\n\n\tversion(Windows)\n\tvoid run() {\n\t\tauto timeout = INFINITE;\n\t\tPseudoblockingOverlappedSocket key;\n\t\tOVERLAPPED* overlapped;\n\t\tDWORD bytes;\n\t\twhile(!globalStopFlag && GetQueuedCompletionStatus(iocp, &bytes, cast(PULONG_PTR) &key, &overlapped, timeout)) {\n\t\t\tif(key is null)\n\t\t\t\tcontinue;\n\t\t\tkey.lastAnswer = bytes;\n\t\t\tif(key.fiber) {\n\t\t\t\tkey.fiber.proceed();\n\t\t\t} else {\n\t\t\t\t// we have a new connection, issue the first receive on it and issue the next accept\n\n\t\t\t\tauto sn = key.accepted;\n\n\t\t\t\tkey.accept();\n\n\t\t\t\tcloexec(sn);\n\t\t\t\tif(lcm.tcp) {\n\t\t\t\t\t// disable Nagle's algorithm to avoid a 40ms delay when we send/recv\n\t\t\t\t\t// on the socket because we do some buffering internally. I think this helps,\n\t\t\t\t\t// certainly does for small requests, and I think it does for larger ones too\n\t\t\t\t\tsn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1);\n\n\t\t\t\t\tsn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!\"seconds\"(10));\n\t\t\t\t}\n\n\t\t\t\tdg(sn);\n\t\t\t}\n\t\t}\n\t\t//SleepEx(INFINITE, TRUE);\n\t}\n\n\tversion(linux)\n\tvoid run() {\n\n\t\timport core.sys.linux.epoll;\n\t\tepfd = epoll_create1(EPOLL_CLOEXEC);\n\t\tif(epfd == -1)\n\t\t\tthrow new Exception(\"epoll_create1 \" ~ to!string(errno));\n\t\tscope(exit) {\n\t\t\timport core.sys.posix.unistd;\n\t\t\tclose(epfd);\n\t\t}\n\n\t\t{\n\t\t\tepoll_event ev;\n\t\t\tev.events = EPOLLIN;\n\t\t\tev.data.fd = cancelfd;\n\t\t\tepoll_ctl(epfd, EPOLL_CTL_ADD, cancelfd, &ev);\n\t\t}\n\n\t\tepoll_event ev;\n\t\tev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough.\n\t\tev.data.fd = lcm.listener.handle;\n\t\tif(epoll_ctl(epfd, EPOLL_CTL_ADD, lcm.listener.handle, &ev) == -1)\n\t\t\tthrow new Exception(\"epoll_ctl \" ~ to!string(errno));\n\n\n\n\t\twhile(!globalStopFlag) {\n\t\t\tSocket sn;\n\n\t\t\tepoll_event[64] events;\n\t\t\tauto nfds = epoll_wait(epfd, events.ptr, events.length, -1);\n\t\t\tif(nfds == -1) {\n\t\t\t\tif(errno == EINTR)\n\t\t\t\t\tcontinue;\n\t\t\t\tthrow new Exception(\"epoll_wait \" ~ to!string(errno));\n\t\t\t}\n\n\t\t\tforeach(idx; 0 .. nfds) {\n\t\t\t\tauto flags = events[idx].events;\n\n\t\t\t\tif(cast(size_t) events[idx].data.ptr == cast(size_t) cancelfd) {\n\t\t\t\t\tglobalStopFlag = true;\n\t\t\t\t\t//import std.stdio; writeln(\"exit heard\");\n\t\t\t\t\tbreak;\n\t\t\t\t} else if(cast(size_t) events[idx].data.ptr == cast(size_t) lcm.listener.handle) {\n\t\t\t\t\t//import std.stdio; writeln(myThreadNumber, \" woken up \", flags);\n\t\t\t\t\t// this try/catch is because it is set to non-blocking mode\n\t\t\t\t\t// and Phobos' stupid api throws an exception instead of returning\n\t\t\t\t\t// if it would block. Why would it block? because a forked process\n\t\t\t\t\t// might have beat us to it, but the wakeup event thundered our herds.\n\t\t\t\t\t\ttry\n\t\t\t\t\t\tsn = lcm.listener.accept(); // don't need to do the acceptCancelable here since the epoll checks it better\n\t\t\t\t\t\tcatch(SocketAcceptException e) { continue; }\n\n\t\t\t\t\tcloexec(sn);\n\t\t\t\t\tif(lcm.tcp) {\n\t\t\t\t\t\t// disable Nagle's algorithm to avoid a 40ms delay when we send/recv\n\t\t\t\t\t\t// on the socket because we do some buffering internally. I think this helps,\n\t\t\t\t\t\t// certainly does for small requests, and I think it does for larger ones too\n\t\t\t\t\t\tsn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1);\n\n\t\t\t\t\t\tsn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!\"seconds\"(10));\n\t\t\t\t\t}\n\n\t\t\t\t\tdg(sn);\n\t\t\t\t} else {\n\t\t\t\t\tif(cast(size_t) events[idx].data.ptr < 1024) {\n\t\t\t\t\t\tthrow new Exception(\"this doesn't look like a fiber pointer...\");\n\t\t\t\t\t}\n\t\t\t\t\tauto fiber = cast(CgiFiber) events[idx].data.ptr;\n\t\t\t\t\tfiber.proceed();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tListeningConnectionManager lcm;\n\tCMT dg;\n\tint myThreadNumber;\n}\n\n\n/* Done with network helper */\n\n/* Helpers for doing temporary files. Used both here and in web.d */\n\nversion(Windows) {\n\timport core.sys.windows.windows;\n\textern(Windows) DWORD GetTempPathW(DWORD, LPWSTR);\n\talias GetTempPathW GetTempPath;\n}\n\nversion(Posix) {\n\tstatic import linux = core.sys.posix.unistd;\n}\n\nstring getTempDirectory() {\n\tstring path;\n\tversion(Windows) {\n\t\twchar[1024] buffer;\n\t\tauto len = GetTempPath(1024, buffer.ptr);\n\t\tif(len == 0)\n\t\t\tthrow new Exception(\"couldn't find a temporary path\");\n\n\t\tauto b = buffer[0 .. len];\n\n\t\tpath = to!string(b);\n\t} else\n\t\tpath = \"/tmp/\";\n\n\treturn path;\n}\n\n\n// I like std.date. These functions help keep my old code and data working with phobos changing.\n\nlong sysTimeToDTime(in SysTime sysTime) {\n    return convert!(\"hnsecs\", \"msecs\")(sysTime.stdTime - 621355968000000000L);\n}\n\nlong dateTimeToDTime(in DateTime dt) {\n\treturn sysTimeToDTime(cast(SysTime) dt);\n}\n\nlong getUtcTime() { // renamed primarily to avoid conflict with std.date itself\n\treturn sysTimeToDTime(Clock.currTime(UTC()));\n}\n\n// NOTE: new SimpleTimeZone(minutes); can perhaps work with the getTimezoneOffset() JS trick\nSysTime dTimeToSysTime(long dTime, immutable TimeZone tz = null) {\n\timmutable hnsecs = convert!(\"msecs\", \"hnsecs\")(dTime) + 621355968000000000L;\n\treturn SysTime(hnsecs, tz);\n}\n\n\n\n// this is a helper to read HTTP transfer-encoding: chunked responses\nimmutable(ubyte[]) dechunk(BufferedInputRange ir) {\n\timmutable(ubyte)[] ret;\n\n\tanother_chunk:\n\t// If here, we are at the beginning of a chunk.\n\tauto a = ir.front();\n\tint chunkSize;\n\tint loc = locationOf(a, \"\\r\\n\");\n\twhile(loc == -1) {\n\t\tir.popFront();\n\t\ta = ir.front();\n\t\tloc = locationOf(a, \"\\r\\n\");\n\t}\n\n\tstring hex;\n\thex = \"\";\n\tfor(int i = 0; i < loc; i++) {\n\t\tchar c = a[i];\n\t\tif(c >= 'A' && c <= 'Z')\n\t\t\tc += 0x20;\n\t\tif((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) {\n\t\t\thex ~= c;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tassert(hex.length);\n\n\tint power = 1;\n\tint size = 0;\n\tforeach(cc1; retro(hex)) {\n\t\tdchar cc = cc1;\n\t\tif(cc >= 'a' && cc <= 'z')\n\t\t\tcc -= 0x20;\n\t\tint val = 0;\n\t\tif(cc >= '0' && cc <= '9')\n\t\t\tval = cc - '0';\n\t\telse\n\t\t\tval = cc - 'A' + 10;\n\n\t\tsize += power * val;\n\t\tpower *= 16;\n\t}\n\n\tchunkSize = size;\n\tassert(size >= 0);\n\n\tif(loc + 2 > a.length) {\n\t\tir.popFront(0, a.length + loc + 2);\n\t\ta = ir.front();\n\t}\n\n\ta = ir.consume(loc + 2);\n\n\tif(chunkSize == 0) { // we're done with the response\n\t\t// if we got here, will change must be true....\n\t\tmore_footers:\n\t\tloc = locationOf(a, \"\\r\\n\");\n\t\tif(loc == -1) {\n\t\t\tir.popFront();\n\t\t\ta = ir.front;\n\t\t\tgoto more_footers;\n\t\t} else {\n\t\t\tassert(loc == 0);\n\t\t\tir.consume(loc + 2);\n\t\t\tgoto finish;\n\t\t}\n\t} else {\n\t\t// if we got here, will change must be true....\n\t\tif(a.length < chunkSize + 2) {\n\t\t\tir.popFront(0, chunkSize + 2);\n\t\t\ta = ir.front();\n\t\t}\n\n\t\tret ~= (a[0..chunkSize]);\n\n\t\tif(!(a.length > chunkSize + 2)) {\n\t\t\tir.popFront(0, chunkSize + 2);\n\t\t\ta = ir.front();\n\t\t}\n\t\tassert(a[chunkSize] == 13);\n\t\tassert(a[chunkSize+1] == 10);\n\t\ta = ir.consume(chunkSize + 2);\n\t\tchunkSize = 0;\n\t\tgoto another_chunk;\n\t}\n\n\tfinish:\n\treturn ret;\n}\n\n// I want to be able to get data from multiple sources the same way...\ninterface ByChunkRange {\n\tbool empty();\n\tvoid popFront();\n\tconst(ubyte)[] front();\n}\n\nByChunkRange byChunk(const(ubyte)[] data) {\n\treturn new class ByChunkRange {\n\t\toverride bool empty() {\n\t\t\treturn !data.length;\n\t\t}\n\n\t\toverride void popFront() {\n\t\t\tif(data.length > 4096)\n\t\t\t\tdata = data[4096 .. $];\n\t\t\telse\n\t\t\t\tdata = null;\n\t\t}\n\n\t\toverride const(ubyte)[] front() {\n\t\t\treturn data[0 .. $ > 4096 ? 4096 : $];\n\t\t}\n\t};\n}\n\nByChunkRange byChunk(BufferedInputRange ir, size_t atMost) {\n\tconst(ubyte)[] f;\n\n\tf = ir.front;\n\tif(f.length > atMost)\n\t\tf = f[0 .. atMost];\n\n\treturn new class ByChunkRange {\n\t\toverride bool empty() {\n\t\t\treturn atMost == 0;\n\t\t}\n\n\t\toverride const(ubyte)[] front() {\n\t\t\treturn f;\n\t\t}\n\n\t\toverride void popFront() {\n\t\t\tir.consume(f.length);\n\t\t\tatMost -= f.length;\n\t\t\tauto a = ir.front();\n\n\t\t\tif(a.length <= atMost) {\n\t\t\t\tf = a;\n\t\t\t\tatMost -= a.length;\n\t\t\t\ta = ir.consume(a.length);\n\t\t\t\tif(atMost != 0)\n\t\t\t\t\tir.popFront();\n\t\t\t\tif(f.length == 0) {\n\t\t\t\t\tf = ir.front();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// we actually have *more* here than we need....\n\t\t\t\tf = a[0..atMost];\n\t\t\t\tatMost = 0;\n\t\t\t\tir.consume(atMost);\n\t\t\t}\n\t\t}\n\t};\n}\n\nversion(cgi_with_websocket) {\n\t// https://tools.ietf.org/html/rfc6455\n\n\t/**\n\t\tWEBSOCKET SUPPORT:\n\n\t\tFull example:\n\t\t---\n\t\t\timport arsd.cgi;\n\n\t\t\tvoid websocketEcho(Cgi cgi) {\n\t\t\t\tif(cgi.websocketRequested()) {\n\t\t\t\t\tif(cgi.origin != \"http://arsdnet.net\")\n\t\t\t\t\t\tthrow new Exception(\"bad origin\");\n\t\t\t\t\tauto websocket = cgi.acceptWebsocket();\n\n\t\t\t\t\twebsocket.send(\"hello\");\n\t\t\t\t\twebsocket.send(\" world!\");\n\n\t\t\t\t\tauto msg = websocket.recv();\n\t\t\t\t\twhile(msg.opcode != WebSocketOpcode.close) {\n\t\t\t\t\t\tif(msg.opcode == WebSocketOpcode.text) {\n\t\t\t\t\t\t\twebsocket.send(msg.textData);\n\t\t\t\t\t\t} else if(msg.opcode == WebSocketOpcode.binary) {\n\t\t\t\t\t\t\twebsocket.send(msg.data);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmsg = websocket.recv();\n\t\t\t\t\t}\n\n\t\t\t\t\twebsocket.close();\n\t\t\t\t} else assert(0, \"i want a web socket!\");\n\t\t\t}\n\n\t\t\tmixin GenericMain!websocketEcho;\n\t\t---\n\t*/\n\n\tclass WebSocket {\n\t\tCgi cgi;\n\n\t\tprivate this(Cgi cgi) {\n\t\t\tthis.cgi = cgi;\n\n\t\t\tSocket socket = cgi.idlol.source;\n\t\t\tsocket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!\"minutes\"(5));\n\t\t}\n\n\t\t// returns true if data available, false if it timed out\n\t\tbool recvAvailable(Duration timeout = dur!\"msecs\"(0)) {\n\t\t\tif(!waitForNextMessageWouldBlock())\n\t\t\t\treturn true;\n\t\t\tif(isDataPending(timeout))\n\t\t\t\treturn true; // this is kinda a lie.\n\n\t\t\treturn false;\n\t\t}\n\n\t\tpublic bool lowLevelReceive() {\n\t\t\tauto bfr = cgi.idlol;\n\t\t\ttop:\n\t\t\tauto got = bfr.front;\n\t\t\tif(got.length) {\n\t\t\t\tif(receiveBuffer.length < receiveBufferUsedLength + got.length)\n\t\t\t\t\treceiveBuffer.length += receiveBufferUsedLength + got.length;\n\n\t\t\t\treceiveBuffer[receiveBufferUsedLength .. receiveBufferUsedLength + got.length] = got[];\n\t\t\t\treceiveBufferUsedLength += got.length;\n\t\t\t\tbfr.consume(got.length);\n\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tif(bfr.sourceClosed)\n\t\t\t\treturn false;\n\n\t\t\tbfr.popFront(0);\n\t\t\tif(bfr.sourceClosed)\n\t\t\t\treturn false;\n\t\t\tgoto top;\n\t\t}\n\n\n\t\tbool isDataPending(Duration timeout = 0.seconds) {\n\t\t\tSocket socket = cgi.idlol.source;\n\n\t\t\tauto check = new SocketSet();\n\t\t\tcheck.add(socket);\n\n\t\t\tauto got = Socket.select(check, null, null, timeout);\n\t\t\tif(got > 0)\n\t\t\t\treturn true;\n\t\t\treturn false;\n\t\t}\n\n\t\t// note: this blocks\n\t\tWebSocketFrame recv() {\n\t\t\treturn waitForNextMessage();\n\t\t}\n\n\n\n\n\t\tprivate void llclose() {\n\t\t\tcgi.close();\n\t\t}\n\n\t\tprivate void llsend(ubyte[] data) {\n\t\t\tcgi.write(data);\n\t\t\tcgi.flush();\n\t\t}\n\n\t\tvoid unregisterActiveSocket(WebSocket) {}\n\n\t\t/* copy/paste section { */\n\n\t\tprivate int readyState_;\n\t\tprivate ubyte[] receiveBuffer;\n\t\tprivate size_t receiveBufferUsedLength;\n\n\t\tprivate Config config;\n\n\t\tenum CONNECTING = 0; /// Socket has been created. The connection is not yet open.\n\t\tenum OPEN = 1; /// The connection is open and ready to communicate.\n\t\tenum CLOSING = 2; /// The connection is in the process of closing.\n\t\tenum CLOSED = 3; /// The connection is closed or couldn't be opened.\n\n\t\t/++\n\n\t\t+/\n\t\t/// Group: foundational\n\t\tstatic struct Config {\n\t\t\t/++\n\t\t\t\tThese control the size of the receive buffer.\n\n\t\t\t\tIt starts at the initial size, will temporarily\n\t\t\t\tballoon up to the maximum size, and will reuse\n\t\t\t\ta buffer up to the likely size.\n\n\t\t\t\tAnything larger than the maximum size will cause\n\t\t\t\tthe connection to be aborted and an exception thrown.\n\t\t\t\tThis is to protect you against a peer trying to\n\t\t\t\texhaust your memory, while keeping the user-level\n\t\t\t\tprocessing simple.\n\t\t\t+/\n\t\t\tsize_t initialReceiveBufferSize = 4096;\n\t\t\tsize_t likelyReceiveBufferSize = 4096; /// ditto\n\t\t\tsize_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto\n\n\t\t\t/++\n\t\t\t\tMaximum combined size of a message.\n\t\t\t+/\n\t\t\tsize_t maximumMessageSize = 10 * 1024 * 1024;\n\n\t\t\tstring[string] cookies; /// Cookies to send with the initial request. cookies[name] = value;\n\t\t\tstring origin; /// Origin URL to send with the handshake, if desired.\n\t\t\tstring protocol; /// the protocol header, if desired.\n\n\t\t\tint pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping\n\t\t}\n\n\t\t/++\n\t\t\tReturns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED].\n\t\t+/\n\t\tint readyState() {\n\t\t\treturn readyState_;\n\t\t}\n\n\t\t/++\n\t\t\tCloses the connection, sending a graceful teardown message to the other side.\n\t\t+/\n\t\t/// Group: foundational\n\t\tvoid close(int code = 0, string reason = null)\n\t\t\t//in (reason.length < 123)\n\t\t\tin { assert(reason.length < 123); } do\n\t\t{\n\t\t\tif(readyState_ != OPEN)\n\t\t\t\treturn; // it cool, we done\n\t\t\tWebSocketFrame wss;\n\t\t\twss.fin = true;\n\t\t\twss.opcode = WebSocketOpcode.close;\n\t\t\twss.data = cast(ubyte[]) reason.dup;\n\t\t\twss.send(&llsend);\n\n\t\t\treadyState_ = CLOSING;\n\n\t\t\tllclose();\n\t\t}\n\n\t\t/++\n\t\t\tSends 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.\n\t\t+/\n\t\t/// Group: foundational\n\t\tvoid ping() {\n\t\t\tWebSocketFrame wss;\n\t\t\twss.fin = true;\n\t\t\twss.opcode = WebSocketOpcode.ping;\n\t\t\twss.send(&llsend);\n\t\t}\n\n\t\t// automatically handled....\n\t\tvoid pong() {\n\t\t\tWebSocketFrame wss;\n\t\t\twss.fin = true;\n\t\t\twss.opcode = WebSocketOpcode.pong;\n\t\t\twss.send(&llsend);\n\t\t}\n\n\t\t/++\n\t\t\tSends a text message through the websocket.\n\t\t+/\n\t\t/// Group: foundational\n\t\tvoid send(in char[] textData) {\n\t\t\tWebSocketFrame wss;\n\t\t\twss.fin = true;\n\t\t\twss.opcode = WebSocketOpcode.text;\n\t\t\twss.data = cast(ubyte[]) textData.dup;\n\t\t\twss.send(&llsend);\n\t\t}\n\n\t\t/++\n\t\t\tSends a binary message through the websocket.\n\t\t+/\n\t\t/// Group: foundational\n\t\tvoid send(in ubyte[] binaryData) {\n\t\t\tWebSocketFrame wss;\n\t\t\twss.fin = true;\n\t\t\twss.opcode = WebSocketOpcode.binary;\n\t\t\twss.data = cast(ubyte[]) binaryData.dup;\n\t\t\twss.send(&llsend);\n\t\t}\n\n\t\t/++\n\t\t\tWaits for and returns the next complete message on the socket.\n\n\t\t\tNote that the onmessage function is still called, right before\n\t\t\tthis returns.\n\t\t+/\n\t\t/// Group: blocking_api\n\t\tpublic WebSocketFrame waitForNextMessage() {\n\t\t\tdo {\n\t\t\t\tauto m = processOnce();\n\t\t\t\tif(m.populated)\n\t\t\t\t\treturn m;\n\t\t\t} while(lowLevelReceive());\n\n\t\t\tthrow new ConnectionClosedException(\"Websocket receive timed out\");\n\t\t\t//return WebSocketFrame.init; // FIXME? maybe.\n\t\t}\n\n\t\t/++\n\t\t\tTells if [waitForNextMessage] would block.\n\t\t+/\n\t\t/// Group: blocking_api\n\t\tpublic bool waitForNextMessageWouldBlock() {\n\t\t\tcheckAgain:\n\t\t\tif(isMessageBuffered())\n\t\t\t\treturn false;\n\t\t\tif(!isDataPending())\n\t\t\t\treturn true;\n\t\t\twhile(isDataPending())\n\t\t\t\tlowLevelReceive();\n\t\t\tgoto checkAgain;\n\t\t}\n\n\t\t/++\n\t\t\tIs there a message in the buffer already?\n\t\t\tIf `true`, [waitForNextMessage] is guaranteed to return immediately.\n\t\t\tIf `false`, check [isDataPending] as the next step.\n\t\t+/\n\t\t/// Group: blocking_api\n\t\tpublic bool isMessageBuffered() {\n\t\t\tubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];\n\t\t\tauto s = d;\n\t\t\tif(d.length) {\n\t\t\t\tauto orig = d;\n\t\t\t\tauto m = WebSocketFrame.read(d);\n\t\t\t\t// that's how it indicates that it needs more data\n\t\t\t\tif(d !is orig)\n\t\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\treturn false;\n\t\t}\n\n\t\tprivate ubyte continuingType;\n\t\tprivate ubyte[] continuingData;\n\t\t//private size_t continuingDataLength;\n\n\t\tprivate WebSocketFrame processOnce() {\n\t\t\tubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength];\n\t\t\tauto s = d;\n\t\t\t// FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer.\n\t\t\tWebSocketFrame m;\n\t\t\tif(d.length) {\n\t\t\t\tauto orig = d;\n\t\t\t\tm = WebSocketFrame.read(d);\n\t\t\t\t// that's how it indicates that it needs more data\n\t\t\t\tif(d is orig)\n\t\t\t\t\treturn WebSocketFrame.init;\n\t\t\t\tm.unmaskInPlace();\n\t\t\t\tswitch(m.opcode) {\n\t\t\t\t\tcase WebSocketOpcode.continuation:\n\t\t\t\t\t\tif(continuingData.length + m.data.length > config.maximumMessageSize)\n\t\t\t\t\t\t\tthrow new Exception(\"message size exceeded\");\n\n\t\t\t\t\t\tcontinuingData ~= m.data;\n\t\t\t\t\t\tif(m.fin) {\n\t\t\t\t\t\t\tif(ontextmessage)\n\t\t\t\t\t\t\t\tontextmessage(cast(char[]) continuingData);\n\t\t\t\t\t\t\tif(onbinarymessage)\n\t\t\t\t\t\t\t\tonbinarymessage(continuingData);\n\n\t\t\t\t\t\t\tcontinuingData = null;\n\t\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t\tcase WebSocketOpcode.text:\n\t\t\t\t\t\tif(m.fin) {\n\t\t\t\t\t\t\tif(ontextmessage)\n\t\t\t\t\t\t\t\tontextmessage(m.textData);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcontinuingType = m.opcode;\n\t\t\t\t\t\t\t//continuingDataLength = 0;\n\t\t\t\t\t\t\tcontinuingData = null;\n\t\t\t\t\t\t\tcontinuingData ~= m.data;\n\t\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t\tcase WebSocketOpcode.binary:\n\t\t\t\t\t\tif(m.fin) {\n\t\t\t\t\t\t\tif(onbinarymessage)\n\t\t\t\t\t\t\t\tonbinarymessage(m.data);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcontinuingType = m.opcode;\n\t\t\t\t\t\t\t//continuingDataLength = 0;\n\t\t\t\t\t\t\tcontinuingData = null;\n\t\t\t\t\t\t\tcontinuingData ~= m.data;\n\t\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t\tcase WebSocketOpcode.close:\n\t\t\t\t\t\treadyState_ = CLOSED;\n\t\t\t\t\t\tif(onclose)\n\t\t\t\t\t\t\tonclose();\n\n\t\t\t\t\t\tunregisterActiveSocket(this);\n\t\t\t\t\tbreak;\n\t\t\t\t\tcase WebSocketOpcode.ping:\n\t\t\t\t\t\tpong();\n\t\t\t\t\tbreak;\n\t\t\t\t\tcase WebSocketOpcode.pong:\n\t\t\t\t\t\t// just really references it is still alive, nbd.\n\t\t\t\t\tbreak;\n\t\t\t\t\tdefault: // ignore though i could and perhaps should throw too\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// the recv thing can be invalidated so gotta copy it over ugh\n\t\t\tif(d.length) {\n\t\t\t\tm.data = m.data.dup();\n\t\t\t}\n\n\t\t\timport core.stdc.string;\n\t\t\tmemmove(receiveBuffer.ptr, d.ptr, d.length);\n\t\t\treceiveBufferUsedLength = d.length;\n\n\t\t\treturn m;\n\t\t}\n\n\t\tprivate void autoprocess() {\n\t\t\t// FIXME\n\t\t\tdo {\n\t\t\t\tprocessOnce();\n\t\t\t} while(lowLevelReceive());\n\t\t}\n\n\n\t\tvoid delegate() onclose; ///\n\t\tvoid delegate() onerror; ///\n\t\tvoid delegate(in char[]) ontextmessage; ///\n\t\tvoid delegate(in ubyte[]) onbinarymessage; ///\n\t\tvoid delegate() onopen; ///\n\n\t\t/++\n\n\t\t+/\n\t\t/// Group: browser_api\n\t\tvoid onmessage(void delegate(in char[]) dg) {\n\t\t\tontextmessage = dg;\n\t\t}\n\n\t\t/// ditto\n\t\tvoid onmessage(void delegate(in ubyte[]) dg) {\n\t\t\tonbinarymessage = dg;\n\t\t}\n\n\t\t/* } end copy/paste */\n\n\n\t}\n\n\tbool websocketRequested(Cgi cgi) {\n\t\treturn\n\t\t\t\"sec-websocket-key\" in cgi.requestHeaders\n\t\t\t&&\n\t\t\t\"connection\" in cgi.requestHeaders &&\n\t\t\t\tcgi.requestHeaders[\"connection\"].asLowerCase().canFind(\"upgrade\")\n\t\t\t&&\n\t\t\t\"upgrade\" in cgi.requestHeaders &&\n\t\t\t\tcgi.requestHeaders[\"upgrade\"].asLowerCase().equal(\"websocket\")\n\t\t\t;\n\t}\n\n\tWebSocket acceptWebsocket(Cgi cgi) {\n\t\tassert(!cgi.closed);\n\t\tassert(!cgi.outputtedResponseData);\n\t\tcgi.setResponseStatus(\"101 Switching Protocols\");\n\t\tcgi.header(\"Upgrade: WebSocket\");\n\t\tcgi.header(\"Connection: upgrade\");\n\n\t\tstring key = cgi.requestHeaders[\"sec-websocket-key\"];\n\t\tkey ~= \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"; // the defined guid from the websocket spec\n\n\t\timport std.digest.sha;\n\t\tauto hash = sha1Of(key);\n\t\tauto accept = Base64.encode(hash);\n\n\t\tcgi.header((\"Sec-WebSocket-Accept: \" ~ accept).idup);\n\n\t\tcgi.websocketMode = true;\n\t\tcgi.write(\"\");\n\n\t\tcgi.flush();\n\n\t\treturn new WebSocket(cgi);\n\t}\n\n\t// FIXME get websocket to work on other modes, not just embedded_httpd\n\n\t/* copy/paste in http2.d { */\n\tenum WebSocketOpcode : ubyte {\n\t\tcontinuation = 0,\n\t\ttext = 1,\n\t\tbinary = 2,\n\t\t// 3, 4, 5, 6, 7 RESERVED\n\t\tclose = 8,\n\t\tping = 9,\n\t\tpong = 10,\n\t\t// 11,12,13,14,15 RESERVED\n\t}\n\n\tpublic struct WebSocketFrame {\n\t\tprivate bool populated;\n\t\tbool fin;\n\t\tbool rsv1;\n\t\tbool rsv2;\n\t\tbool rsv3;\n\t\tWebSocketOpcode opcode; // 4 bits\n\t\tbool masked;\n\t\tubyte lengthIndicator; // don't set this when building one to send\n\t\tulong realLength; // don't use when sending\n\t\tubyte[4] maskingKey; // don't set this when sending\n\t\tubyte[] data;\n\n\t\tstatic WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) {\n\t\t\tWebSocketFrame msg;\n\t\t\tmsg.fin = true;\n\t\t\tmsg.opcode = opcode;\n\t\t\tmsg.data = cast(ubyte[]) data.dup;\n\n\t\t\treturn msg;\n\t\t}\n\n\t\tprivate void send(scope void delegate(ubyte[]) llsend) {\n\t\t\tubyte[64] headerScratch;\n\t\t\tint headerScratchPos = 0;\n\n\t\t\trealLength = data.length;\n\n\t\t\t{\n\t\t\t\tubyte b1;\n\t\t\t\tb1 |= cast(ubyte) opcode;\n\t\t\t\tb1 |= rsv3 ? (1 << 4) : 0;\n\t\t\t\tb1 |= rsv2 ? (1 << 5) : 0;\n\t\t\t\tb1 |= rsv1 ? (1 << 6) : 0;\n\t\t\t\tb1 |= fin  ? (1 << 7) : 0;\n\n\t\t\t\theaderScratch[0] = b1;\n\t\t\t\theaderScratchPos++;\n\t\t\t}\n\n\t\t\t{\n\t\t\t\theaderScratchPos++; // we'll set header[1] at the end of this\n\t\t\t\tauto rlc = realLength;\n\t\t\t\tubyte b2;\n\t\t\t\tb2 |= masked ? (1 << 7) : 0;\n\n\t\t\t\tassert(headerScratchPos == 2);\n\n\t\t\t\tif(realLength > 65535) {\n\t\t\t\t\t// use 64 bit length\n\t\t\t\t\tb2 |= 0x7f;\n\n\t\t\t\t\t// FIXME: double check endinaness\n\t\t\t\t\tforeach(i; 0 .. 8) {\n\t\t\t\t\t\theaderScratch[2 + 7 - i] = rlc & 0x0ff;\n\t\t\t\t\t\trlc >>>= 8;\n\t\t\t\t\t}\n\n\t\t\t\t\theaderScratchPos += 8;\n\t\t\t\t} else if(realLength > 125) {\n\t\t\t\t\t// use 16 bit length\n\t\t\t\t\tb2 |= 0x7e;\n\n\t\t\t\t\t// FIXME: double check endinaness\n\t\t\t\t\tforeach(i; 0 .. 2) {\n\t\t\t\t\t\theaderScratch[2 + 1 - i] = rlc & 0x0ff;\n\t\t\t\t\t\trlc >>>= 8;\n\t\t\t\t\t}\n\n\t\t\t\t\theaderScratchPos += 2;\n\t\t\t\t} else {\n\t\t\t\t\t// use 7 bit length\n\t\t\t\t\tb2 |= realLength & 0b_0111_1111;\n\t\t\t\t}\n\n\t\t\t\theaderScratch[1] = b2;\n\t\t\t}\n\n\t\t\t//assert(!masked, \"masking key not properly implemented\");\n\t\t\tif(masked) {\n\t\t\t\t// FIXME: randomize this\n\t\t\t\theaderScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[];\n\t\t\t\theaderScratchPos += 4;\n\n\t\t\t\t// we'll just mask it in place...\n\t\t\t\tint keyIdx = 0;\n\t\t\t\tforeach(i; 0 .. data.length) {\n\t\t\t\t\tdata[i] = data[i] ^ maskingKey[keyIdx];\n\t\t\t\t\tif(keyIdx == 3)\n\t\t\t\t\t\tkeyIdx = 0;\n\t\t\t\t\telse\n\t\t\t\t\t\tkeyIdx++;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t//writeln(\"SENDING \", headerScratch[0 .. headerScratchPos], data);\n\t\t\tllsend(headerScratch[0 .. headerScratchPos]);\n\t\t\tllsend(data);\n\t\t}\n\n\t\tstatic WebSocketFrame read(ref ubyte[] d) {\n\t\t\tWebSocketFrame msg;\n\n\t\t\tauto orig = d;\n\n\t\t\tWebSocketFrame needsMoreData() {\n\t\t\t\td = orig;\n\t\t\t\treturn WebSocketFrame.init;\n\t\t\t}\n\n\t\t\tif(d.length < 2)\n\t\t\t\treturn needsMoreData();\n\n\t\t\tubyte b = d[0];\n\n\t\t\tmsg.populated = true;\n\n\t\t\tmsg.opcode = cast(WebSocketOpcode) (b & 0x0f);\n\t\t\tb >>= 4;\n\t\t\tmsg.rsv3 = b & 0x01;\n\t\t\tb >>= 1;\n\t\t\tmsg.rsv2 = b & 0x01;\n\t\t\tb >>= 1;\n\t\t\tmsg.rsv1 = b & 0x01;\n\t\t\tb >>= 1;\n\t\t\tmsg.fin = b & 0x01;\n\n\t\t\tb = d[1];\n\t\t\tmsg.masked = (b & 0b1000_0000) ? true : false;\n\t\t\tmsg.lengthIndicator = b & 0b0111_1111;\n\n\t\t\td = d[2 .. $];\n\n\t\t\tif(msg.lengthIndicator == 0x7e) {\n\t\t\t\t// 16 bit length\n\t\t\t\tmsg.realLength = 0;\n\n\t\t\t\tif(d.length < 2) return needsMoreData();\n\n\t\t\t\tforeach(i; 0 .. 2) {\n\t\t\t\t\tmsg.realLength |= d[0] << ((1-i) * 8);\n\t\t\t\t\td = d[1 .. $];\n\t\t\t\t}\n\t\t\t} else if(msg.lengthIndicator == 0x7f) {\n\t\t\t\t// 64 bit length\n\t\t\t\tmsg.realLength = 0;\n\n\t\t\t\tif(d.length < 8) return needsMoreData();\n\n\t\t\t\tforeach(i; 0 .. 8) {\n\t\t\t\t\tmsg.realLength |= ulong(d[0]) << ((7-i) * 8);\n\t\t\t\t\td = d[1 .. $];\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 7 bit length\n\t\t\t\tmsg.realLength = msg.lengthIndicator;\n\t\t\t}\n\n\t\t\tif(msg.masked) {\n\n\t\t\t\tif(d.length < 4) return needsMoreData();\n\n\t\t\t\tmsg.maskingKey = d[0 .. 4];\n\t\t\t\td = d[4 .. $];\n\t\t\t}\n\n\t\t\tif(msg.realLength > d.length) {\n\t\t\t\treturn needsMoreData();\n\t\t\t}\n\n\t\t\tmsg.data = d[0 .. cast(size_t) msg.realLength];\n\t\t\td = d[cast(size_t) msg.realLength .. $];\n\n\t\t\treturn msg;\n\t\t}\n\n\t\tvoid unmaskInPlace() {\n\t\t\tif(this.masked) {\n\t\t\t\tint keyIdx = 0;\n\t\t\t\tforeach(i; 0 .. this.data.length) {\n\t\t\t\t\tthis.data[i] = this.data[i] ^ this.maskingKey[keyIdx];\n\t\t\t\t\tif(keyIdx == 3)\n\t\t\t\t\t\tkeyIdx = 0;\n\t\t\t\t\telse\n\t\t\t\t\t\tkeyIdx++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tchar[] textData() {\n\t\t\treturn cast(char[]) data;\n\t\t}\n\t}\n\t/* } */\n}\n\n\nversion(Windows)\n{\n    version(CRuntime_DigitalMars)\n    {\n        extern(C) int setmode(int, int) nothrow @nogc;\n    }\n    else version(CRuntime_Microsoft)\n    {\n        extern(C) int _setmode(int, int) nothrow @nogc;\n        alias setmode = _setmode;\n    }\n    else static assert(0);\n}\n\nversion(Posix) {\n\timport core.sys.posix.unistd;\n\tversion(CRuntime_Musl) {} else {\n\t\tprivate extern(C) int posix_spawn(pid_t*, const char*, void*, void*, const char**, const char**);\n\t}\n}\n\n\n// FIXME: these aren't quite public yet.\n//private:\n\n// template for laziness\nvoid startAddonServer()(string arg) {\n\tversion(OSX) {\n\t\tassert(0, \"Not implemented\");\n\t} else version(linux) {\n\t\timport core.sys.posix.unistd;\n\t\tpid_t pid;\n\t\tconst(char)*[16] args;\n\t\targs[0] = \"ARSD_CGI_ADDON_SERVER\";\n\t\targs[1] = arg.ptr;\n\t\tposix_spawn(&pid, \"/proc/self/exe\",\n\t\t\tnull,\n\t\t\tnull,\n\t\t\targs.ptr,\n\t\t\tnull // env\n\t\t);\n\t} else version(Windows) {\n\t\twchar[2048] filename;\n\t\tauto len = GetModuleFileNameW(null, filename.ptr, cast(DWORD) filename.length);\n\t\tif(len == 0 || len == filename.length)\n\t\t\tthrow new Exception(\"could not get process name to start helper server\");\n\n\t\tSTARTUPINFOW startupInfo;\n\t\tstartupInfo.cb = cast(DWORD) startupInfo.sizeof;\n\t\tPROCESS_INFORMATION processInfo;\n\n\t\timport std.utf;\n\n\t\t// I *MIGHT* need to run it as a new job or a service...\n\t\tauto ret = CreateProcessW(\n\t\t\tfilename.ptr,\n\t\t\ttoUTF16z(arg),\n\t\t\tnull, // process attributes\n\t\t\tnull, // thread attributes\n\t\t\tfalse, // inherit handles\n\t\t\t0, // creation flags\n\t\t\tnull, // environment\n\t\t\tnull, // working directory\n\t\t\t&startupInfo,\n\t\t\t&processInfo\n\t\t);\n\n\t\tif(!ret)\n\t\t\tthrow new Exception(\"create process failed\");\n\n\t\t// when done with those, if we set them\n\t\t/*\n\t\tCloseHandle(hStdInput);\n\t\tCloseHandle(hStdOutput);\n\t\tCloseHandle(hStdError);\n\t\t*/\n\n\t} else static assert(0, \"Websocket server not implemented on this system yet (email me, i can prolly do it if you need it)\");\n}\n\n// template for laziness\n/*\n\tThe websocket server is a single-process, single-thread, event\n\tI/O thing. It is passed websockets from other CGI processes\n\tand is then responsible for handling their messages and responses.\n\tNote that the CGI process is responsible for websocket setup,\n\tincluding authentication, etc.\n\n\tIt also gets data sent to it by other processes and is responsible\n\tfor distributing that, as necessary.\n*/\nvoid runWebsocketServer()() {\n\tassert(0, \"not implemented\");\n}\n\nvoid sendToWebsocketServer(WebSocket ws, string group) {\n\tassert(0, \"not implemented\");\n}\n\nvoid sendToWebsocketServer(string content, string group) {\n\tassert(0, \"not implemented\");\n}\n\n\nvoid runEventServer()() {\n\trunAddonServer(\"/tmp/arsd_cgi_event_server\", new EventSourceServerImplementation());\n}\n\nvoid runTimerServer()() {\n\trunAddonServer(\"/tmp/arsd_scheduled_job_server\", new ScheduledJobServerImplementation());\n}\n\nversion(Posix) {\n\talias LocalServerConnectionHandle = int;\n\talias CgiConnectionHandle = int;\n\talias SocketConnectionHandle = int;\n\n\tenum INVALID_CGI_CONNECTION_HANDLE = -1;\n} else version(Windows) {\n\talias LocalServerConnectionHandle = HANDLE;\n\tversion(embedded_httpd_threads) {\n\t\talias CgiConnectionHandle = SOCKET;\n\t\tenum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET;\n\t} else version(fastcgi) {\n\t\talias CgiConnectionHandle = void*; // Doesn't actually work! But I don't want compile to fail pointlessly at this point.\n\t\tenum INVALID_CGI_CONNECTION_HANDLE = null;\n\t} else version(scgi) {\n\t\talias CgiConnectionHandle = SOCKET;\n\t\tenum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET;\n\t} else { /* version(plain_cgi) */\n\t\talias CgiConnectionHandle = HANDLE;\n\t\tenum INVALID_CGI_CONNECTION_HANDLE = null;\n\t}\n\talias SocketConnectionHandle = SOCKET;\n}\n\nversion(with_addon_servers_connections)\nLocalServerConnectionHandle openLocalServerConnection()(string name, string arg) {\n\tversion(Posix) {\n\t\timport core.sys.posix.unistd;\n\t\timport core.sys.posix.sys.un;\n\n\t\tint sock = socket(AF_UNIX, SOCK_STREAM, 0);\n\t\tif(sock == -1)\n\t\t\tthrow new Exception(\"socket \" ~ to!string(errno));\n\n\t\tscope(failure)\n\t\t\tclose(sock);\n\n\t\tcloexec(sock);\n\n\t\t// add-on server processes are assumed to be local, and thus will\n\t\t// use unix domain sockets. Besides, I want to pass sockets to them,\n\t\t// so it basically must be local (except for the session server, but meh).\n\t\tsockaddr_un addr;\n\t\taddr.sun_family = AF_UNIX;\n\t\tversion(linux) {\n\t\t\t// on linux, we will use the abstract namespace\n\t\t\taddr.sun_path[0] = 0;\n\t\t\taddr.sun_path[1 .. name.length + 1] = cast(typeof(addr.sun_path[])) name[];\n\t\t} else {\n\t\t\t// but otherwise, just use a file cuz we must.\n\t\t\taddr.sun_path[0 .. name.length] = cast(typeof(addr.sun_path[])) name[];\n\t\t}\n\n\t\tbool alreadyTried;\n\n\t\ttry_again:\n\n\t\tif(connect(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) {\n\t\t\tif(!alreadyTried && errno == ECONNREFUSED) {\n\t\t\t\t// try auto-spawning the server, then attempt connection again\n\t\t\t\tstartAddonServer(arg);\n\t\t\t\timport core.thread;\n\t\t\t\tThread.sleep(50.msecs);\n\t\t\t\talreadyTried = true;\n\t\t\t\tgoto try_again;\n\t\t\t} else\n\t\t\t\tthrow new Exception(\"connect \" ~ to!string(errno));\n\t\t}\n\n\t\treturn sock;\n\t} else version(Windows) {\n\t\treturn null; // FIXME\n\t}\n}\n\nversion(with_addon_servers_connections)\nvoid closeLocalServerConnection(LocalServerConnectionHandle handle) {\n\tversion(Posix) {\n\t\timport core.sys.posix.unistd;\n\t\tclose(handle);\n\t} else version(Windows)\n\t\tCloseHandle(handle);\n}\n\nvoid runSessionServer()() {\n\trunAddonServer(\"/tmp/arsd_session_server\", new BasicDataServerImplementation());\n}\n\nversion(Posix)\nprivate void makeNonBlocking(int fd) {\n\timport core.sys.posix.fcntl;\n\tauto flags = fcntl(fd, F_GETFL, 0);\n\tif(flags == -1)\n\t\tthrow new Exception(\"fcntl get\");\n\tflags |= O_NONBLOCK;\n\tauto s = fcntl(fd, F_SETFL, flags);\n\tif(s == -1)\n\t\tthrow new Exception(\"fcntl set\");\n}\n\nimport core.stdc.errno;\n\nstruct IoOp {\n\t@disable this();\n\t@disable this(this);\n\n\t/*\n\t\tSo we want to be able to eventually handle generic sockets too.\n\t*/\n\n\tenum Read = 1;\n\tenum Write = 2;\n\tenum Accept = 3;\n\tenum ReadSocketHandle = 4;\n\n\t// Your handler may be called in a different thread than the one that initiated the IO request!\n\t// It is also possible to have multiple io requests being called simultaneously. Use proper thread safety caution.\n\tprivate bool delegate(IoOp*, int) handler; // returns true if you are done and want it to be closed\n\tprivate void delegate(IoOp*) closeHandler;\n\tprivate void delegate(IoOp*) completeHandler;\n\tprivate int internalFd;\n\tprivate int operation;\n\tprivate int bufferLengthAllocated;\n\tprivate int bufferLengthUsed;\n\tprivate ubyte[1] internalBuffer; // it can be overallocated!\n\n\tubyte[] allocatedBuffer() return {\n\t\treturn internalBuffer.ptr[0 .. bufferLengthAllocated];\n\t}\n\n\tubyte[] usedBuffer() return {\n\t\treturn allocatedBuffer[0 .. bufferLengthUsed];\n\t}\n\n\tvoid reset() {\n\t\tbufferLengthUsed = 0;\n\t}\n\n\tint fd() {\n\t\treturn internalFd;\n\t}\n}\n\nIoOp* allocateIoOp(int fd, int operation, int bufferSize, bool delegate(IoOp*, int) handler) {\n\timport core.stdc.stdlib;\n\n\tauto ptr = calloc(IoOp.sizeof + bufferSize, 1);\n\tif(ptr is null)\n\t\tassert(0); // out of memory!\n\n\tauto op = cast(IoOp*) ptr;\n\n\top.handler = handler;\n\top.internalFd = fd;\n\top.operation = operation;\n\top.bufferLengthAllocated = bufferSize;\n\top.bufferLengthUsed = 0;\n\n\timport core.memory;\n\n\tGC.addRoot(ptr);\n\n\treturn op;\n}\n\nvoid freeIoOp(ref IoOp* ptr) {\n\n\timport core.memory;\n\tGC.removeRoot(ptr);\n\n\timport core.stdc.stdlib;\n\tfree(ptr);\n\tptr = null;\n}\n\nversion(Posix)\nversion(with_addon_servers_connections)\nvoid nonBlockingWrite(EventIoServer eis, int connection, const void[] data) {\n\n\t//import std.stdio : writeln; writeln(cast(string) data);\n\n\timport core.sys.posix.unistd;\n\n\tauto ret = write(connection, data.ptr, data.length);\n\tif(ret != data.length) {\n\t\tif(ret == 0 || (ret == -1 && (errno == EPIPE || errno == ETIMEDOUT))) {\n\t\t\t// the file is closed, remove it\n\t\t\teis.fileClosed(connection);\n\t\t} else\n\t\t\tthrow new Exception(\"alas \" ~ to!string(ret) ~ \" \" ~ to!string(errno)); // FIXME\n\t}\n}\nversion(Windows)\nversion(with_addon_servers_connections)\nvoid nonBlockingWrite(EventIoServer eis, int connection, const void[] data) {\n\t// FIXME\n}\n\nbool isInvalidHandle(CgiConnectionHandle h) {\n\treturn h == INVALID_CGI_CONNECTION_HANDLE;\n}\n\n/+\nhttps://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsarecv\nhttps://support.microsoft.com/en-gb/help/181611/socket-overlapped-i-o-versus-blocking-nonblocking-mode\nhttps://stackoverflow.com/questions/18018489/should-i-use-iocps-or-overlapped-wsasend-receive\nhttps://docs.microsoft.com/en-us/windows/desktop/fileio/i-o-completion-ports\nhttps://docs.microsoft.com/en-us/windows/desktop/fileio/createiocompletionport\nhttps://docs.microsoft.com/en-us/windows/desktop/api/mswsock/nf-mswsock-acceptex\nhttps://docs.microsoft.com/en-us/windows/desktop/Sync/waitable-timer-objects\nhttps://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-setwaitabletimer\nhttps://docs.microsoft.com/en-us/windows/desktop/Sync/using-a-waitable-timer-with-an-asynchronous-procedure-call\nhttps://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsagetoverlappedresult\n\n+/\n\n/++\n\tYou can customize your server by subclassing the appropriate server. Then, register your\n\tsubclass at compile time with the [registerEventIoServer] template, or implement your own\n\tmain function and call it yourself.\n\n\t$(TIP If you make your subclass a `final class`, there is a slight performance improvement.)\n+/\nversion(with_addon_servers_connections)\ninterface EventIoServer {\n\tbool handleLocalConnectionData(IoOp* op, int receivedFd);\n\tvoid handleLocalConnectionClose(IoOp* op);\n\tvoid handleLocalConnectionComplete(IoOp* op);\n\tvoid wait_timeout();\n\tvoid fileClosed(int fd);\n\n\tvoid epoll_fd(int fd);\n}\n\n// the sink should buffer it\nprivate void serialize(T)(scope void delegate(scope ubyte[]) sink, T t) {\n\tstatic if(is(T == struct)) {\n\t\tforeach(member; __traits(allMembers, T))\n\t\t\tserialize(sink, __traits(getMember, t, member));\n\t} else static if(is(T : int)) {\n\t\t// no need to think of endianness just because this is only used\n\t\t// for local, same-machine stuff anyway. thanks private lol\n\t\tsink((cast(ubyte*) &t)[0 .. t.sizeof]);\n\t} else static if(is(T == string) || is(T : const(ubyte)[])) {\n\t\t// these are common enough to optimize\n\t\tint len = cast(int) t.length; // want length consistent size tho, in case 32 bit program sends to 64 bit server, etc.\n\t\tsink((cast(ubyte*) &len)[0 .. int.sizeof]);\n\t\tsink(cast(ubyte[]) t[]);\n\t} else static if(is(T : A[], A)) {\n\t\t// generic array is less optimal but still prolly ok\n\t\tint len = cast(int) t.length;\n\t\tsink((cast(ubyte*) &len)[0 .. int.sizeof]);\n\t\tforeach(item; t)\n\t\t\tserialize(sink, item);\n\t} else static assert(0, T.stringof);\n}\n\n// all may be stack buffers, so use caution\nprivate void deserialize(T)(scope ubyte[] delegate(int sz) get, scope void delegate(T) dg) {\n\tstatic if(is(T == struct)) {\n\t\tT t;\n\t\tforeach(member; __traits(allMembers, T))\n\t\t\tdeserialize!(typeof(__traits(getMember, T, member)))(get, (mbr) { __traits(getMember, t, member) = mbr; });\n\t\tdg(t);\n\t} else static if(is(T : int)) {\n\t\t// no need to think of endianness just because this is only used\n\t\t// for local, same-machine stuff anyway. thanks private lol\n\t\tT t;\n\t\tauto data = get(t.sizeof);\n\t\tt = (cast(T[]) data)[0];\n\t\tdg(t);\n\t} else static if(is(T == string) || is(T : const(ubyte)[])) {\n\t\t// these are common enough to optimize\n\t\tint len;\n\t\tauto data = get(len.sizeof);\n\t\tlen = (cast(int[]) data)[0];\n\n\t\t/*\n\t\ttypeof(T[0])[2000] stackBuffer;\n\t\tT buffer;\n\n\t\tif(len < stackBuffer.length)\n\t\t\tbuffer = stackBuffer[0 .. len];\n\t\telse\n\t\t\tbuffer = new T(len);\n\n\t\tdata = get(len * typeof(T[0]).sizeof);\n\t\t*/\n\n\t\tT t = cast(T) get(len * cast(int) typeof(T.init[0]).sizeof);\n\n\t\tdg(t);\n\t} else static if(is(T == E[], E)) {\n\t\tT t;\n\t\tint len;\n\t\tauto data = get(len.sizeof);\n\t\tlen = (cast(int[]) data)[0];\n\t\tt.length = len;\n\t\tforeach(ref e; t) {\n\t\t\tdeserialize!E(get, (ele) { e = ele; });\n\t\t}\n\t\tdg(t);\n\t} else static assert(0, T.stringof);\n}\n\nunittest {\n\tserialize((ubyte[] b) {\n\t\tdeserialize!int( sz => b[0 .. sz], (t) { assert(t == 1); });\n\t}, 1);\n\tserialize((ubyte[] b) {\n\t\tdeserialize!int( sz => b[0 .. sz], (t) { assert(t == 56674); });\n\t}, 56674);\n\tubyte[1000] buffer;\n\tint bufferPoint;\n\tvoid add(ubyte[] b) {\n\t\tbuffer[bufferPoint ..  bufferPoint + b.length] = b[];\n\t\tbufferPoint += b.length;\n\t}\n\tubyte[] get(int sz) {\n\t\tauto b = buffer[bufferPoint .. bufferPoint + sz];\n\t\tbufferPoint += sz;\n\t\treturn b;\n\t}\n\tserialize(&add, \"test here\");\n\tbufferPoint = 0;\n\tdeserialize!string(&get, (t) { assert(t == \"test here\"); });\n\tbufferPoint = 0;\n\n\tstruct Foo {\n\t\tint a;\n\t\tubyte c;\n\t\tstring d;\n\t}\n\tserialize(&add, Foo(403, 37, \"amazing\"));\n\tbufferPoint = 0;\n\tdeserialize!Foo(&get, (t) {\n\t\tassert(t.a == 403);\n\t\tassert(t.c == 37);\n\t\tassert(t.d == \"amazing\");\n\t});\n\tbufferPoint = 0;\n}\n\n/*\n\tHere's the way the RPC interface works:\n\n\tYou define the interface that lists the functions you can call on the remote process.\n\tThe interface may also have static methods for convenience. These forward to a singleton\n\tinstance of an auto-generated class, which actually sends the args over the pipe.\n\n\tAn impl class actually implements it. A receiving server deserializes down the pipe and\n\tcalls methods on the class.\n\n\tI went with the interface to get some nice compiler checking and documentation stuff.\n\n\tI could have skipped the interface and just implemented it all from the server class definition\n\titself, but then the usage may call the method instead of rpcing it; I just like having the user\n\tinterface and the implementation separate so you aren't tempted to `new impl` to call the methods.\n\n\n\tI 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.\n\n\tRealistically though the bodies would just be\n\t\tconnection.call(this.mangleof, args...) sooooo.\n\n\tFIXME: overloads aren't supported\n*/\n\n/// Base for storing sessions in an array. Exists primarily for internal purposes and you should generally not use this.\ninterface SessionObject {}\n\nprivate immutable void delegate(string[])[string] scheduledJobHandlers;\nprivate immutable void delegate(string[])[string] websocketServers;\n\nversion(with_breaking_cgi_features)\nmixin(q{\n\nmixin template ImplementRpcClientInterface(T, string serverPath, string cmdArg) {\n\tstatic import std.traits;\n\n\t// 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.\n\tstatic foreach(idx, member; __traits(derivedMembers, T)) {\n\tstatic if(__traits(isVirtualMethod, __traits(getMember, T, member)))\n\t\tmixin( q{\n\t\tstd.traits.ReturnType!(__traits(getMember, T, member))\n\t\t} ~ member ~ q{(std.traits.Parameters!(__traits(getMember, T, member)) params)\n\t\t{\n\t\t\tSerializationBuffer buffer;\n\t\t\tauto i = cast(ushort) idx;\n\t\t\tserialize(&buffer.sink, i);\n\t\t\tserialize(&buffer.sink, __traits(getMember, T, member).mangleof);\n\t\t\tforeach(param; params)\n\t\t\t\tserialize(&buffer.sink, param);\n\n\t\t\tauto sendable = buffer.sendable;\n\n\t\t\tversion(Posix) {{\n\t\t\t\tauto ret = send(connectionHandle, sendable.ptr, sendable.length, 0);\n\n\t\t\t\tif(ret == -1) {\n\t\t\t\t\tthrow new Exception(\"send returned -1, errno: \" ~ to!string(errno));\n\t\t\t\t} else if(ret == 0) {\n\t\t\t\t\tthrow new Exception(\"Connection to addon server lost\");\n\t\t\t\t} if(ret < sendable.length)\n\t\t\t\t\tthrow new Exception(\"Send failed to send all\");\n\t\t\t\tassert(ret == sendable.length);\n\t\t\t}} // FIXME Windows impl\n\n\t\t\tstatic if(!is(typeof(return) == void)) {\n\t\t\t\t// there is a return value; we need to wait for it too\n\t\t\t\tversion(Posix) {\n\t\t\t\t\tubyte[3000] revBuffer;\n\t\t\t\t\tauto ret = recv(connectionHandle, revBuffer.ptr, revBuffer.length, 0);\n\t\t\t\t\tauto got = revBuffer[0 .. ret];\n\n\t\t\t\t\tint dataLocation;\n\t\t\t\t\tubyte[] grab(int sz) {\n\t\t\t\t\t\tauto dataLocation1 = dataLocation;\n\t\t\t\t\t\tdataLocation += sz;\n\t\t\t\t\t\treturn got[dataLocation1 .. dataLocation];\n\t\t\t\t\t}\n\n\t\t\t\t\ttypeof(return) retu;\n\t\t\t\t\tdeserialize!(typeof(return))(&grab, (a) { retu = a; });\n\t\t\t\t\treturn retu;\n\t\t\t\t} else {\n\t\t\t\t\t// FIXME Windows impl\n\t\t\t\t\treturn typeof(return).init;\n\t\t\t\t}\n\n\t\t\t}\n\t\t}});\n\t}\n\n\tprivate static typeof(this) singletonInstance;\n\tprivate LocalServerConnectionHandle connectionHandle;\n\n\tstatic typeof(this) connection() {\n\t\tif(singletonInstance is null) {\n\t\t\tsingletonInstance = new typeof(this)();\n\t\t\tsingletonInstance.connect();\n\t\t}\n\t\treturn singletonInstance;\n\t}\n\n\tvoid connect() {\n\t\tconnectionHandle = openLocalServerConnection(serverPath, cmdArg);\n\t}\n\n\tvoid disconnect() {\n\t\tcloseLocalServerConnection(connectionHandle);\n\t}\n}\n\nvoid dispatchRpcServer(Interface, Class)(Class this_, ubyte[] data, int fd) if(is(Class : Interface)) {\n\tushort calledIdx;\n\tstring calledFunction;\n\n\tint dataLocation;\n\tubyte[] grab(int sz) {\n\t\tif(sz == 0) assert(0);\n\t\tauto d = data[dataLocation .. dataLocation + sz];\n\t\tdataLocation += sz;\n\t\treturn d;\n\t}\n\n\tagain:\n\n\tdeserialize!ushort(&grab, (a) { calledIdx = a; });\n\tdeserialize!string(&grab, (a) { calledFunction = a; });\n\n\timport std.traits;\n\n\tsw: switch(calledIdx) {\n\t\tforeach(idx, memberName; __traits(derivedMembers, Interface))\n\t\tstatic if(__traits(isVirtualMethod, __traits(getMember, Interface, memberName))) {\n\t\t\tcase idx:\n\t\t\t\tassert(calledFunction == __traits(getMember, Interface, memberName).mangleof);\n\n\t\t\t\tParameters!(__traits(getMember, Interface, memberName)) params;\n\t\t\t\tforeach(ref param; params)\n\t\t\t\t\tdeserialize!(typeof(param))(&grab, (a) { param = a; });\n\n\t\t\t\tstatic if(is(ReturnType!(__traits(getMember, Interface, memberName)) == void)) {\n\t\t\t\t\t__traits(getMember, this_, memberName)(params);\n\t\t\t\t} else {\n\t\t\t\t\tauto ret = __traits(getMember, this_, memberName)(params);\n\t\t\t\t\tSerializationBuffer buffer;\n\t\t\t\t\tserialize(&buffer.sink, ret);\n\n\t\t\t\t\tauto sendable = buffer.sendable;\n\n\t\t\t\t\tversion(Posix) {\n\t\t\t\t\t\tauto r = send(fd, sendable.ptr, sendable.length, 0);\n\t\t\t\t\t\tif(r == -1) {\n\t\t\t\t\t\t\tthrow new Exception(\"send returned -1, errno: \" ~ to!string(errno));\n\t\t\t\t\t\t} else if(r == 0) {\n\t\t\t\t\t\t\tthrow new Exception(\"Connection to addon client lost\");\n\t\t\t\t\t\t} if(r < sendable.length)\n\t\t\t\t\t\t\tthrow new Exception(\"Send failed to send all\");\n\n\t\t\t\t\t} // FIXME Windows impl\n\t\t\t\t}\n\t\t\tbreak sw;\n\t\t}\n\t\tdefault: assert(0);\n\t}\n\n\tif(dataLocation != data.length)\n\t\tgoto again;\n}\n\n\nprivate struct SerializationBuffer {\n\tubyte[2048] bufferBacking;\n\tint bufferLocation;\n\tvoid sink(scope ubyte[] data) {\n\t\tbufferBacking[bufferLocation .. bufferLocation + data.length] = data[];\n\t\tbufferLocation += data.length;\n\t}\n\n\tubyte[] sendable() return {\n\t\treturn bufferBacking[0 .. bufferLocation];\n\t}\n}\n\n/*\n\tFIXME:\n\t\tadd a version command line arg\n\t\tversion data in the library\n\t\tmanagement gui as external program\n\n\t\tat server with event_fd for each run\n\t\tuse .mangleof in the at function name\n\n\t\ti think the at server will have to:\n\t\t\tpipe args to the child\n\t\t\tcollect child output for logging\n\t\t\tget child return value for logging\n\n\t\t\ton windows timers work differently. idk how to best combine with the io stuff.\n\n\t\t\twill have to have dump and restore too, so i can restart without losing stuff.\n*/\n\n/++\n\tA convenience object for talking to the [BasicDataServer] from a higher level.\n\tSee: [Cgi.getSessionObject].\n\n\tYou pass it a `Data` struct describing the data you want saved in the session.\n\tThen, this class will generate getter and setter properties that allow access\n\tto that data.\n\n\tNote that each load and store will be done as-accessed; it doesn't front-load\n\tmutable data nor does it batch updates out of fear of read-modify-write race\n\tconditions. (In fact, right now it does this for everything, but in the future,\n\tI might batch load `immutable` members of the Data struct.)\n\n\tAt some point in the future, I might also let it do different backends, like\n\ta client-side cookie store too, but idk.\n\n\tNote that the plain-old-data members of your `Data` struct are wrapped by this\n\tinterface via a static foreach to make property functions.\n\n\tSee_Also: [MockSession]\n+/\ninterface Session(Data) : SessionObject {\n\t@property string sessionId() const;\n\n\t/++\n\t\tStarts a new session. Note that a session is also\n\t\timplicitly started as soon as you write data to it,\n\t\tso if you need to alter these parameters from their\n\t\tdefaults, be sure to explicitly call this BEFORE doing\n\t\tany writes to session data.\n\n\t\tParams:\n\t\t\tidleLifetime = How long, in seconds, the session\n\t\t\tshould remain in memory when not being read from\n\t\t\tor written to. The default is one day.\n\n\t\t\tNOT IMPLEMENTED\n\n\t\t\tuseExtendedLifetimeCookie = The session ID is always\n\t\t\tstored in a HTTP cookie, and by default, that cookie\n\t\t\tis discarded when the user closes their browser.\n\n\t\t\tBut if you set this to true, it will use a non-perishable\n\t\t\tcookie for the given idleLifetime.\n\n\t\t\tNOT IMPLEMENTED\n\t+/\n\tvoid start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false);\n\n\t/++\n\t\tRegenerates the session ID and updates the associated\n\t\tcookie.\n\n\t\tThis is also your chance to change immutable data\n\t\t(not yet implemented).\n\t+/\n\tvoid regenerateId();\n\n\t/++\n\t\tTerminates this session, deleting all saved data.\n\t+/\n\tvoid terminate();\n\n\t/++\n\t\tPlain-old-data members of your `Data` struct are wrapped here via\n\t\tthe property getters and setters.\n\n\t\tIf the member is a non-string array, it returns a magical array proxy\n\t\tobject which allows for atomic appends and replaces via overloaded operators.\n\t\tYou can slice this to get a range representing a $(B const) view of the array.\n\t\tThis is to protect you against read-modify-write race conditions.\n\t+/\n\tstatic foreach(memberName; __traits(allMembers, Data))\n\t\tstatic if(is(typeof(__traits(getMember, Data, memberName))))\n\t\tmixin(q{\n\t\t\t@property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout;\n\t\t\t@property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value);\n\t\t});\n\n}\n\n/++\n\tAn implementation of [Session] that works on real cgi connections utilizing the\n\t[BasicDataServer].\n\n\tAs opposed to a [MockSession] which is made for testing purposes.\n\n\tYou will not construct one of these directly. See [Cgi.getSessionObject] instead.\n+/\nclass BasicDataServerSession(Data) : Session!Data {\n\tprivate Cgi cgi;\n\tprivate string sessionId_;\n\n\tpublic @property string sessionId() const {\n\t\treturn sessionId_;\n\t}\n\n\tprotected @property string sessionId(string s) {\n\t\treturn this.sessionId_ = s;\n\t}\n\n\tprivate this(Cgi cgi) {\n\t\tthis.cgi = cgi;\n\t\tif(auto ptr = \"sessionId\" in cgi.cookies)\n\t\t\tsessionId = (*ptr).length ? *ptr : null;\n\t}\n\n\tvoid start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {\n\t\tassert(sessionId is null);\n\n\t\t// FIXME: what if there is a session ID cookie, but no corresponding session on the server?\n\n\t\timport std.random, std.conv;\n\t\tsessionId = to!string(uniform(1, long.max));\n\n\t\tBasicDataServer.connection.createSession(sessionId, idleLifetime);\n\t\tsetCookie();\n\t}\n\n\tprotected void setCookie() {\n\t\tcgi.setCookie(\n\t\t\t\"sessionId\", sessionId,\n\t\t\t0 /* expiration */,\n\t\t\t\"/\" /* path */,\n\t\t\tnull /* domain */,\n\t\t\ttrue /* http only */,\n\t\t\tcgi.https /* if the session is started on https, keep it there, otherwise, be flexible */);\n\t}\n\n\tvoid regenerateId() {\n\t\tif(sessionId is null) {\n\t\t\tstart();\n\t\t\treturn;\n\t\t}\n\t\timport std.random, std.conv;\n\t\tauto oldSessionId = sessionId;\n\t\tsessionId = to!string(uniform(1, long.max));\n\t\tBasicDataServer.connection.renameSession(oldSessionId, sessionId);\n\t\tsetCookie();\n\t}\n\n\tvoid terminate() {\n\t\tBasicDataServer.connection.destroySession(sessionId);\n\t\tsessionId = null;\n\t\tsetCookie();\n\t}\n\n\tstatic foreach(memberName; __traits(allMembers, Data))\n\t\tstatic if(is(typeof(__traits(getMember, Data, memberName))))\n\t\tmixin(q{\n\t\t\t@property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout {\n\t\t\t\tif(sessionId is null)\n\t\t\t\t\treturn typeof(return).init;\n\n\t\t\t\timport std.traits;\n\t\t\t\tauto v = BasicDataServer.connection.getSessionData(sessionId, fullyQualifiedName!Data ~ \".\" ~ memberName);\n\t\t\t\tif(v.length == 0)\n\t\t\t\t\treturn typeof(return).init;\n\t\t\t\timport std.conv;\n\t\t\t\t// why this cast? to doesn't like being given an inout argument. so need to do it without that, then\n\t\t\t\t// we need to return it and that needed the cast. It should be fine since we basically respect constness..\n\t\t\t\t// basically. Assuming the session is POD this should be fine.\n\t\t\t\treturn cast(typeof(return)) to!(typeof(__traits(getMember, Data, memberName)))(v);\n\t\t\t}\n\t\t\t@property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) {\n\t\t\t\tif(sessionId is null)\n\t\t\t\t\tstart();\n\t\t\t\timport std.conv;\n\t\t\t\timport std.traits;\n\t\t\t\tBasicDataServer.connection.setSessionData(sessionId, fullyQualifiedName!Data ~ \".\" ~ memberName, to!string(value));\n\t\t\t\treturn value;\n\t\t\t}\n\t\t});\n}\n\n/++\n\tA mock object that works like the real session, but doesn't actually interact with any actual database or http connection.\n\tSimply stores the data in its instance members.\n+/\nclass MockSession(Data) : Session!Data {\n\tpure {\n\t\t@property string sessionId() const { return \"mock\"; }\n\t\tvoid start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {}\n\t\tvoid regenerateId() {}\n\t\tvoid terminate() {}\n\n\t\tprivate Data store_;\n\n\t\tstatic foreach(memberName; __traits(allMembers, Data))\n\t\t\tstatic if(is(typeof(__traits(getMember, Data, memberName))))\n\t\t\tmixin(q{\n\t\t\t\t@property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout {\n\t\t\t\t\treturn __traits(getMember, store_, memberName);\n\t\t\t\t}\n\t\t\t\t@property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) {\n\t\t\t\t\treturn __traits(getMember, store_, memberName) = value;\n\t\t\t\t}\n\t\t\t});\n\t}\n}\n\n/++\n\tDirect interface to the basic data add-on server. You can\n\ttypically use [Cgi.getSessionObject] as a more convenient interface.\n+/\nversion(with_addon_servers_connections)\ninterface BasicDataServer {\n\t///\n\tvoid createSession(string sessionId, int lifetime);\n\t///\n\tvoid renewSession(string sessionId, int lifetime);\n\t///\n\tvoid destroySession(string sessionId);\n\t///\n\tvoid renameSession(string oldSessionId, string newSessionId);\n\n\t///\n\tvoid setSessionData(string sessionId, string dataKey, string dataValue);\n\t///\n\tstring getSessionData(string sessionId, string dataKey);\n\n\t///\n\tstatic BasicDataServerConnection connection() {\n\t\treturn BasicDataServerConnection.connection();\n\t}\n}\n\nversion(with_addon_servers_connections)\nclass BasicDataServerConnection : BasicDataServer {\n\tmixin ImplementRpcClientInterface!(BasicDataServer, \"/tmp/arsd_session_server\", \"--session-server\");\n}\n\nversion(with_addon_servers)\nfinal class BasicDataServerImplementation : BasicDataServer, EventIoServer {\n\n\tvoid createSession(string sessionId, int lifetime) {\n\t\tsessions[sessionId.idup] = Session(lifetime);\n\t}\n\tvoid destroySession(string sessionId) {\n\t\tsessions.remove(sessionId);\n\t}\n\tvoid renewSession(string sessionId, int lifetime) {\n\t\tsessions[sessionId].lifetime = lifetime;\n\t}\n\tvoid renameSession(string oldSessionId, string newSessionId) {\n\t\tsessions[newSessionId.idup] = sessions[oldSessionId];\n\t\tsessions.remove(oldSessionId);\n\t}\n\tvoid setSessionData(string sessionId, string dataKey, string dataValue) {\n\t\tif(sessionId !in sessions)\n\t\t\tcreateSession(sessionId, 3600); // FIXME?\n\t\tsessions[sessionId].values[dataKey.idup] = dataValue.idup;\n\t}\n\tstring getSessionData(string sessionId, string dataKey) {\n\t\tif(auto session = sessionId in sessions) {\n\t\t\tif(auto data = dataKey in (*session).values)\n\t\t\t\treturn *data;\n\t\t\telse\n\t\t\t\treturn null; // no such data\n\n\t\t} else {\n\t\t\treturn null; // no session\n\t\t}\n\t}\n\n\n\tprotected:\n\n\tstruct Session {\n\t\tint lifetime;\n\n\t\tstring[string] values;\n\t}\n\n\tSession[string] sessions;\n\n\tbool handleLocalConnectionData(IoOp* op, int receivedFd) {\n\t\tauto data = op.usedBuffer;\n\t\tdispatchRpcServer!BasicDataServer(this, data, op.fd);\n\t\treturn false;\n\t}\n\n\tvoid handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go\n\tvoid handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant\n\tvoid wait_timeout() {}\n\tvoid fileClosed(int fd) {} // stateless so irrelevant\n\tvoid epoll_fd(int fd) {}\n}\n\n/++\n\tSee [schedule] to make one of these. You then call one of the methods here to set it up:\n\n\t---\n\t\tschedule!fn(args).at(DateTime(2019, 8, 7, 12, 00, 00)); // run the function at August 7, 2019, 12 noon UTC\n\t\tschedule!fn(args).delay(6.seconds); // run it after waiting 6 seconds\n\t\tschedule!fn(args).asap(); // run it in the background as soon as the event loop gets around to it\n\t---\n+/\nversion(with_addon_servers_connections)\nstruct ScheduledJobHelper {\n\tprivate string func;\n\tprivate string[] args;\n\tprivate bool consumed;\n\n\tprivate this(string func, string[] args) {\n\t\tthis.func = func;\n\t\tthis.args = args;\n\t}\n\n\t~this() {\n\t\tassert(consumed);\n\t}\n\n\t/++\n\t\tSchedules the job to be run at the given time.\n\t+/\n\tvoid at(DateTime when, immutable TimeZone timezone = UTC()) {\n\t\tconsumed = true;\n\n\t\tauto conn = ScheduledJobServerConnection.connection;\n\t\timport std.file;\n\t\tauto st = SysTime(when, timezone);\n\t\tauto jobId = conn.scheduleJob(1, cast(int) st.toUnixTime(), thisExePath, func, args);\n\t}\n\n\t/++\n\t\tSchedules the job to run at least after the specified delay.\n\t+/\n\tvoid delay(Duration delay) {\n\t\tconsumed = true;\n\n\t\tauto conn = ScheduledJobServerConnection.connection;\n\t\timport std.file;\n\t\tauto jobId = conn.scheduleJob(0, cast(int) delay.total!\"seconds\", thisExePath, func, args);\n\t}\n\n\t/++\n\t\tRuns the job in the background ASAP.\n\n\t\t$(NOTE It may run in a background thread. Don't segfault!)\n\t+/\n\tvoid asap() {\n\t\tconsumed = true;\n\n\t\tauto conn = ScheduledJobServerConnection.connection;\n\t\timport std.file;\n\t\tauto jobId = conn.scheduleJob(0, 1, thisExePath, func, args);\n\t}\n\n\t/+\n\t/++\n\t\tSchedules the job to recur on the given pattern.\n\t+/\n\tvoid recur(string spec) {\n\n\t}\n\t+/\n}\n\n/++\n\tFirst step to schedule a job on the scheduled job server.\n\n\tThe scheduled job needs to be a top-level function that doesn't read any\n\tvariables from outside its arguments because it may be run in a new process,\n\twithout any context existing later.\n\n\tYou MUST set details on the returned object to actually do anything!\n+/\ntemplate schedule(alias fn, T...) if(is(typeof(fn) == function)) {\n\t///\n\tScheduledJobHelper schedule(T args) {\n\t\t// this isn't meant to ever be called, but instead just to\n\t\t// get the compiler to type check the arguments passed for us\n\t\tauto sample = delegate() {\n\t\t\tfn(args);\n\t\t};\n\t\tstring[] sargs;\n\t\tforeach(arg; args)\n\t\t\tsargs ~= to!string(arg);\n\t\treturn ScheduledJobHelper(fn.mangleof, sargs);\n\t}\n\n\tshared static this() {\n\t\tscheduledJobHandlers[fn.mangleof] = delegate(string[] sargs) {\n\t\t\timport std.traits;\n\t\t\tParameters!fn args;\n\t\t\tforeach(idx, ref arg; args)\n\t\t\t\targ = to!(typeof(arg))(sargs[idx]);\n\t\t\tfn(args);\n\t\t};\n\t}\n}\n\n///\ninterface ScheduledJobServer {\n\t/// Use the [schedule] function for a higher-level interface.\n\tint scheduleJob(int whenIs, int when, string executable, string func, string[] args);\n\t///\n\tvoid cancelJob(int jobId);\n}\n\nversion(with_addon_servers_connections)\nclass ScheduledJobServerConnection : ScheduledJobServer {\n\tmixin ImplementRpcClientInterface!(ScheduledJobServer, \"/tmp/arsd_scheduled_job_server\", \"--timer-server\");\n}\n\nversion(with_addon_servers)\nfinal class ScheduledJobServerImplementation : ScheduledJobServer, EventIoServer {\n\t// FIXME: we need to handle SIGCHLD in this somehow\n\t// whenIs is 0 for relative, 1 for absolute\n\tprotected int scheduleJob(int whenIs, int when, string executable, string func, string[] args) {\n\t\tauto nj = nextJobId;\n\t\tnextJobId++;\n\n\t\tversion(linux) {\n\t\t\timport core.sys.linux.timerfd;\n\t\t\timport core.sys.linux.epoll;\n\t\t\timport core.sys.posix.unistd;\n\n\n\t\t\tauto fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC);\n\t\t\tif(fd == -1)\n\t\t\t\tthrow new Exception(\"fd timer create failed\");\n\n\t\t\tforeach(ref arg; args)\n\t\t\t\targ = arg.idup;\n\t\t\tauto job = Job(executable.idup, func.idup, .dup(args), fd, nj);\n\n\t\t\titimerspec value;\n\t\t\tvalue.it_value.tv_sec = when;\n\t\t\tvalue.it_value.tv_nsec = 0;\n\n\t\t\tvalue.it_interval.tv_sec = 0;\n\t\t\tvalue.it_interval.tv_nsec = 0;\n\n\t\t\tif(timerfd_settime(fd, whenIs == 1 ? TFD_TIMER_ABSTIME : 0, &value, null) == -1)\n\t\t\t\tthrow new Exception(\"couldn't set fd timer\");\n\n\t\t\tauto op = allocateIoOp(fd, IoOp.Read, 16, (IoOp* op, int fd) {\n\t\t\t\tjobs.remove(nj);\n\t\t\t\tepoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, null);\n\t\t\t\tclose(fd);\n\n\n\t\t\t\tspawnProcess([job.executable, \"--timed-job\", job.func] ~ job.args);\n\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\tscope(failure)\n\t\t\t\tfreeIoOp(op);\n\n\t\t\tepoll_event ev;\n\t\t\tev.events = EPOLLIN | EPOLLET;\n\t\t\tev.data.ptr = op;\n\t\t\tif(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1)\n\t\t\t\tthrow new Exception(\"epoll_ctl \" ~ to!string(errno));\n\n\t\t\tjobs[nj] = job;\n\t\t\treturn nj;\n\t\t} else assert(0);\n\t}\n\n\tprotected void cancelJob(int jobId) {\n\t\tversion(linux) {\n\t\t\tauto job = jobId in jobs;\n\t\t\tif(job is null)\n\t\t\t\treturn;\n\n\t\t\tjobs.remove(jobId);\n\n\t\t\tversion(linux) {\n\t\t\t\timport core.sys.linux.timerfd;\n\t\t\t\timport core.sys.linux.epoll;\n\t\t\t\timport core.sys.posix.unistd;\n\t\t\t\tepoll_ctl(epoll_fd, EPOLL_CTL_DEL, job.timerfd, null);\n\t\t\t\tclose(job.timerfd);\n\t\t\t}\n\t\t}\n\t\tjobs.remove(jobId);\n\t}\n\n\tint nextJobId = 1;\n\tstatic struct Job {\n\t\tstring executable;\n\t\tstring func;\n\t\tstring[] args;\n\t\tint timerfd;\n\t\tint id;\n\t}\n\tJob[int] jobs;\n\n\n\t// event io server methods below\n\n\tbool handleLocalConnectionData(IoOp* op, int receivedFd) {\n\t\tauto data = op.usedBuffer;\n\t\tdispatchRpcServer!ScheduledJobServer(this, data, op.fd);\n\t\treturn false;\n\t}\n\n\tvoid handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go\n\tvoid handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant\n\tvoid wait_timeout() {}\n\tvoid fileClosed(int fd) {} // stateless so irrelevant\n\n\tint epoll_fd_;\n\tvoid epoll_fd(int fd) {this.epoll_fd_ = fd; }\n\tint epoll_fd() { return epoll_fd_; }\n}\n\n///\nversion(with_addon_servers_connections)\ninterface EventSourceServer {\n\t/++\n\t\tsends 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.\n\n\t\t$(WARNING This API is extremely unstable. I might change it or remove it without notice.)\n\n\t\tSee_Also:\n\t\t\t[sendEvent]\n\t+/\n\tpublic static void adoptConnection(Cgi cgi, in char[] eventUrl) {\n\t\t/*\n\t\t\tIf lastEventId is missing or empty, you just get new events as they come.\n\n\t\t\tIf it is set from something else, it sends all since then (that are still alive)\n\t\t\tdown the pipe immediately.\n\n\t\t\tThe reason it can come from the header is that's what the standard defines for\n\t\t\tbrowser reconnects. The reason it can come from a query string is just convenience\n\t\t\tin catching up in a user-defined manner.\n\n\t\t\tThe reason the header overrides the query string is if the browser tries to reconnect,\n\t\t\tit will send the header AND the query (it reconnects to the same url), so we just\n\t\t\twant to do the restart thing.\n\n\t\t\tNote that if you ask for \"0\" as the lastEventId, it will get ALL still living events.\n\t\t*/\n\t\tstring lastEventId = cgi.lastEventId;\n\t\tif(lastEventId.length == 0 && \"lastEventId\" in cgi.get)\n\t\t\tlastEventId = cgi.get[\"lastEventId\"];\n\n\t\tcgi.setResponseContentType(\"text/event-stream\");\n\t\tcgi.write(\":\\n\", false); // to initialize the chunking and send headers before keeping the fd for later\n\t\tcgi.flush();\n\n\t\tcgi.closed = true;\n\t\tauto s = openLocalServerConnection(\"/tmp/arsd_cgi_event_server\", \"--event-server\");\n\t\tscope(exit)\n\t\t\tcloseLocalServerConnection(s);\n\n\t\tversion(fastcgi)\n\t\t\tthrow new Exception(\"sending fcgi connections not supported\");\n\t\telse {\n\t\t\tauto fd = cgi.getOutputFileHandle();\n\t\t\tif(isInvalidHandle(fd))\n\t\t\t\tthrow new Exception(\"bad fd from cgi!\");\n\n\t\t\tEventSourceServerImplementation.SendableEventConnection sec;\n\t\t\tsec.populate(cgi.responseChunked, eventUrl, lastEventId);\n\n\t\t\tversion(Posix) {\n\t\t\t\tauto res = write_fd(s, cast(void*) &sec, sec.sizeof, fd);\n\t\t\t\tassert(res == sec.sizeof);\n\t\t\t} else version(Windows) {\n\t\t\t\t// FIXME\n\t\t\t}\n\t\t}\n\t}\n\n\t/++\n\t\tSends 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.\n\n\t\t$(WARNING This API is extremely unstable. I might change it or remove it without notice.)\n\n\t\tParams:\n\t\t\turl = 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.\n\t\t\tevent = the event type string, which is used in the Javascript addEventListener API on EventSource\n\t\t\tdata = the event data. Available in JS as `event.data`.\n\t\t\tlifetime = the amount of time to keep this event for replaying on the event server.\n\n\t\tSee_Also:\n\t\t\t[sendEventToEventServer]\n\t+/\n\tpublic static void sendEvent(string url, string event, string data, int lifetime) {\n\t\tauto s = openLocalServerConnection(\"/tmp/arsd_cgi_event_server\", \"--event-server\");\n\t\tscope(exit)\n\t\t\tcloseLocalServerConnection(s);\n\n\t\tEventSourceServerImplementation.SendableEvent sev;\n\t\tsev.populate(url, event, data, lifetime);\n\n\t\tversion(Posix) {\n\t\t\tauto ret = send(s, &sev, sev.sizeof, 0);\n\t\t\tassert(ret == sev.sizeof);\n\t\t} else version(Windows) {\n\t\t\t// FIXME\n\t\t}\n\t}\n\n\t/++\n\t\tMessages sent to `url` will also be sent to anyone listening on `forwardUrl`.\n\n\t\tSee_Also: [disconnect]\n\t+/\n\tvoid connect(string url, string forwardUrl);\n\n\t/++\n\t\tDisconnects `forwardUrl` from `url`\n\n\t\tSee_Also: [connect]\n\t+/\n\tvoid disconnect(string url, string forwardUrl);\n}\n\n///\nversion(with_addon_servers)\nfinal class EventSourceServerImplementation : EventSourceServer, EventIoServer {\n\n\tprotected:\n\n\tvoid connect(string url, string forwardUrl) {\n\t\tpipes[url] ~= forwardUrl;\n\t}\n\tvoid disconnect(string url, string forwardUrl) {\n\t\tauto t = url in pipes;\n\t\tif(t is null)\n\t\t\treturn;\n\t\tforeach(idx, n; (*t))\n\t\t\tif(n == forwardUrl) {\n\t\t\t\t(*t)[idx] = (*t)[$-1];\n\t\t\t\t(*t) = (*t)[0 .. $-1];\n\t\t\t\tbreak;\n\t\t\t}\n\t}\n\n\tbool handleLocalConnectionData(IoOp* op, int receivedFd) {\n\t\tif(receivedFd != -1) {\n\t\t\t//writeln(\"GOT FD \", receivedFd, \" -- \", op.usedBuffer);\n\n\t\t\t//core.sys.posix.unistd.write(receivedFd, \"hello\".ptr, 5);\n\n\t\t\tSendableEventConnection* got = cast(SendableEventConnection*) op.usedBuffer.ptr;\n\n\t\t\tauto url = got.url.idup;\n\t\t\teventConnectionsByUrl[url] ~= EventConnection(receivedFd, got.responseChunked > 0 ? true : false);\n\n\t\t\t// FIXME: catch up on past messages here\n\t\t} else {\n\t\t\tauto data = op.usedBuffer;\n\t\t\tauto event = cast(SendableEvent*) data.ptr;\n\n\t\t\tif(event.magic == 0xdeadbeef) {\n\t\t\t\thandleInputEvent(event);\n\n\t\t\t\tif(event.url in pipes)\n\t\t\t\tforeach(pipe; pipes[event.url]) {\n\t\t\t\t\tevent.url = pipe;\n\t\t\t\t\thandleInputEvent(event);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdispatchRpcServer!EventSourceServer(this, data, op.fd);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\tvoid handleLocalConnectionClose(IoOp* op) {\n\t\tfileClosed(op.fd);\n\t}\n\tvoid handleLocalConnectionComplete(IoOp* op) {}\n\n\tvoid wait_timeout() {\n\t\t// just keeping alive\n\t\tforeach(url, connections; eventConnectionsByUrl)\n\t\tforeach(connection; connections)\n\t\t\tif(connection.needsChunking)\n\t\t\t\tnonBlockingWrite(this, connection.fd, \"1b\\r\\nevent: keepalive\\ndata: ok\\n\\n\\r\\n\");\n\t\t\telse\n\t\t\t\tnonBlockingWrite(this, connection.fd, \"event: keepalive\\ndata: ok\\n\\n\\r\\n\");\n\t}\n\n\tvoid fileClosed(int fd) {\n\t\touter: foreach(url, ref connections; eventConnectionsByUrl) {\n\t\t\tforeach(idx, conn; connections) {\n\t\t\t\tif(fd == conn.fd) {\n\t\t\t\t\tconnections[idx] = connections[$-1];\n\t\t\t\t\tconnections = connections[0 .. $ - 1];\n\t\t\t\t\tcontinue outer;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid epoll_fd(int fd) {}\n\n\n\tprivate:\n\n\n\tstruct SendableEventConnection {\n\t\tubyte responseChunked;\n\n\t\tint urlLength;\n\t\tchar[256] urlBuffer = 0;\n\n\t\tint lastEventIdLength;\n\t\tchar[32] lastEventIdBuffer = 0;\n\n\t\tchar[] url() return {\n\t\t\treturn urlBuffer[0 .. urlLength];\n\t\t}\n\t\tvoid url(in char[] u) {\n\t\t\turlBuffer[0 .. u.length] = u[];\n\t\t\turlLength = cast(int) u.length;\n\t\t}\n\t\tchar[] lastEventId() return {\n\t\t\treturn lastEventIdBuffer[0 .. lastEventIdLength];\n\t\t}\n\t\tvoid populate(bool responseChunked, in char[] url, in char[] lastEventId)\n\t\tin {\n\t\t\tassert(url.length < this.urlBuffer.length);\n\t\t\tassert(lastEventId.length < this.lastEventIdBuffer.length);\n\t\t}\n\t\tdo {\n\t\t\tthis.responseChunked = responseChunked ? 1 : 0;\n\t\t\tthis.urlLength = cast(int) url.length;\n\t\t\tthis.lastEventIdLength = cast(int) lastEventId.length;\n\n\t\t\tthis.urlBuffer[0 .. url.length] = url[];\n\t\t\tthis.lastEventIdBuffer[0 .. lastEventId.length] = lastEventId[];\n\t\t}\n\t}\n\n\tstruct SendableEvent {\n\t\tint magic = 0xdeadbeef;\n\t\tint urlLength;\n\t\tchar[256] urlBuffer = 0;\n\t\tint typeLength;\n\t\tchar[32] typeBuffer = 0;\n\t\tint messageLength;\n\t\tchar[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.\n\t\tint _lifetime;\n\n\t\tchar[] message() return {\n\t\t\treturn messageBuffer[0 .. messageLength];\n\t\t}\n\t\tchar[] type() return {\n\t\t\treturn typeBuffer[0 .. typeLength];\n\t\t}\n\t\tchar[] url() return {\n\t\t\treturn urlBuffer[0 .. urlLength];\n\t\t}\n\t\tvoid url(in char[] u) {\n\t\t\turlBuffer[0 .. u.length] = u[];\n\t\t\turlLength = cast(int) u.length;\n\t\t}\n\t\tint lifetime() {\n\t\t\treturn _lifetime;\n\t\t}\n\n\t\t///\n\t\tvoid populate(string url, string type, string message, int lifetime)\n\t\tin {\n\t\t\tassert(url.length < this.urlBuffer.length);\n\t\t\tassert(type.length < this.typeBuffer.length);\n\t\t\tassert(message.length < this.messageBuffer.length);\n\t\t}\n\t\tdo {\n\t\t\tthis.urlLength = cast(int) url.length;\n\t\t\tthis.typeLength = cast(int) type.length;\n\t\t\tthis.messageLength = cast(int) message.length;\n\t\t\tthis._lifetime = lifetime;\n\n\t\t\tthis.urlBuffer[0 .. url.length] = url[];\n\t\t\tthis.typeBuffer[0 .. type.length] = type[];\n\t\t\tthis.messageBuffer[0 .. message.length] = message[];\n\t\t}\n\t}\n\n\tstruct EventConnection {\n\t\tint fd;\n\t\tbool needsChunking;\n\t}\n\n\tprivate EventConnection[][string] eventConnectionsByUrl;\n\tprivate string[][string] pipes;\n\n\tprivate void handleInputEvent(scope SendableEvent* event) {\n\t\tstatic int eventId;\n\n\t\tstatic struct StoredEvent {\n\t\t\tint id;\n\t\t\tstring type;\n\t\t\tstring message;\n\t\t\tint lifetimeRemaining;\n\t\t}\n\n\t\tStoredEvent[][string] byUrl;\n\n\t\tint thisId = ++eventId;\n\n\t\tif(event.lifetime)\n\t\t\tbyUrl[event.url.idup] ~= StoredEvent(thisId, event.type.idup, event.message.idup, event.lifetime);\n\n\t\tauto connectionsPtr = event.url in eventConnectionsByUrl;\n\t\tEventConnection[] connections;\n\t\tif(connectionsPtr is null)\n\t\t\treturn;\n\t\telse\n\t\t\tconnections = *connectionsPtr;\n\n\t\tchar[4096] buffer;\n\t\tchar[] formattedMessage;\n\n\t\tvoid append(const char[] a) {\n\t\t\t// the 6's here are to leave room for a HTTP chunk header, if it proves necessary\n\t\t\tbuffer[6 + formattedMessage.length .. 6 + formattedMessage.length + a.length] = a[];\n\t\t\tformattedMessage = buffer[6 .. 6 + formattedMessage.length + a.length];\n\t\t}\n\n\t\timport std.algorithm.iteration;\n\n\t\tif(connections.length) {\n\t\t\tappend(\"id: \");\n\t\t\tappend(to!string(thisId));\n\t\t\tappend(\"\\n\");\n\n\t\t\tappend(\"event: \");\n\t\t\tappend(event.type);\n\t\t\tappend(\"\\n\");\n\n\t\t\tforeach(line; event.message.splitter(\"\\n\")) {\n\t\t\t\tappend(\"data: \");\n\t\t\t\tappend(line);\n\t\t\t\tappend(\"\\n\");\n\t\t\t}\n\n\t\t\tappend(\"\\n\");\n\t\t}\n\n\t\t// chunk it for HTTP!\n\t\tauto len = toHex(formattedMessage.length);\n\t\tbuffer[4 .. 6] = \"\\r\\n\"[];\n\t\tbuffer[4 - len.length .. 4] = len[];\n\t\tbuffer[6 + formattedMessage.length] = '\\r';\n\t\tbuffer[6 + formattedMessage.length + 1] = '\\n';\n\n\t\tauto chunkedMessage = buffer[4 - len.length .. 6 + formattedMessage.length +2];\n\t\t// done\n\n\t\t// FIXME: send back requests when needed\n\t\t// FIXME: send a single \":\\n\" every 15 seconds to keep alive\n\n\t\tforeach(connection; connections) {\n\t\t\tif(connection.needsChunking) {\n\t\t\t\tnonBlockingWrite(this, connection.fd, chunkedMessage);\n\t\t\t} else {\n\t\t\t\tnonBlockingWrite(this, connection.fd, formattedMessage);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoServer)) {\n\tversion(Posix) {\n\n\t\timport core.sys.posix.unistd;\n\t\timport core.sys.posix.fcntl;\n\t\timport core.sys.posix.sys.un;\n\n\t\timport core.sys.posix.signal;\n\t\tsignal(SIGPIPE, SIG_IGN);\n\n\t\tstatic extern(C) void sigchldhandler(int) {\n\t\t\tint status;\n\t\t\timport w = core.sys.posix.sys.wait;\n\t\t\tw.wait(&status);\n\t\t}\n\t\tsignal(SIGCHLD, &sigchldhandler);\n\n\t\tint sock = socket(AF_UNIX, SOCK_STREAM, 0);\n\t\tif(sock == -1)\n\t\t\tthrow new Exception(\"socket \" ~ to!string(errno));\n\n\t\tscope(failure)\n\t\t\tclose(sock);\n\n\t\tcloexec(sock);\n\n\t\t// add-on server processes are assumed to be local, and thus will\n\t\t// use unix domain sockets. Besides, I want to pass sockets to them,\n\t\t// so it basically must be local (except for the session server, but meh).\n\t\tsockaddr_un addr;\n\t\taddr.sun_family = AF_UNIX;\n\t\tversion(linux) {\n\t\t\t// on linux, we will use the abstract namespace\n\t\t\taddr.sun_path[0] = 0;\n\t\t\taddr.sun_path[1 .. localListenerName.length + 1] = cast(typeof(addr.sun_path[])) localListenerName[];\n\t\t} else {\n\t\t\t// but otherwise, just use a file cuz we must.\n\t\t\taddr.sun_path[0 .. localListenerName.length] = cast(typeof(addr.sun_path[])) localListenerName[];\n\t\t}\n\n\t\tif(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1)\n\t\t\tthrow new Exception(\"bind \" ~ to!string(errno));\n\n\t\tif(listen(sock, 128) == -1)\n\t\t\tthrow new Exception(\"listen \" ~ to!string(errno));\n\n\t\tmakeNonBlocking(sock);\n\n\t\tversion(linux) {\n\t\t\timport core.sys.linux.epoll;\n\t\t\tauto epoll_fd = epoll_create1(EPOLL_CLOEXEC);\n\t\t\tif(epoll_fd == -1)\n\t\t\t\tthrow new Exception(\"epoll_create1 \" ~ to!string(errno));\n\t\t\tscope(failure)\n\t\t\t\tclose(epoll_fd);\n\t\t} else {\n\t\t\timport core.sys.posix.poll;\n\t\t}\n\n\t\tversion(linux)\n\t\teis.epoll_fd = epoll_fd;\n\n\t\tauto acceptOp = allocateIoOp(sock, IoOp.Read, 0, null);\n\t\tscope(exit)\n\t\t\tfreeIoOp(acceptOp);\n\n\t\tversion(linux) {\n\t\t\tepoll_event ev;\n\t\t\tev.events = EPOLLIN | EPOLLET;\n\t\t\tev.data.ptr = acceptOp;\n\t\t\tif(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev) == -1)\n\t\t\t\tthrow new Exception(\"epoll_ctl \" ~ to!string(errno));\n\n\t\t\tepoll_event[64] events;\n\t\t} else {\n\t\t\tpollfd[] pollfds;\n\t\t\tIoOp*[int] ioops;\n\t\t\tpollfds ~= pollfd(sock, POLLIN);\n\t\t\tioops[sock] = acceptOp;\n\t\t}\n\n\t\timport core.time : MonoTime, seconds;\n\n\t\tMonoTime timeout = MonoTime.currTime + 15.seconds;\n\n\t\twhile(true) {\n\n\t\t\t// FIXME: it should actually do a timerfd that runs on any thing that hasn't been run recently\n\n\t\t\tint timeout_milliseconds = 0; //  -1; // infinite\n\n\t\t\ttimeout_milliseconds = cast(int) (timeout - MonoTime.currTime).total!\"msecs\";\n\t\t\tif(timeout_milliseconds < 0)\n\t\t\t\ttimeout_milliseconds = 0;\n\n\t\t\t//writeln(\"waiting for \", name);\n\n\t\t\tversion(linux) {\n\t\t\t\tauto nfds = epoll_wait(epoll_fd, events.ptr, events.length, timeout_milliseconds);\n\t\t\t\tif(nfds == -1) {\n\t\t\t\t\tif(errno == EINTR)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tthrow new Exception(\"epoll_wait \" ~ to!string(errno));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tint nfds = poll(pollfds.ptr, cast(int) pollfds.length, timeout_milliseconds);\n\t\t\t\tsize_t lastIdx = 0;\n\t\t\t}\n\n\t\t\tif(nfds == 0) {\n\t\t\t\teis.wait_timeout();\n\t\t\t\ttimeout += 15.seconds;\n\t\t\t}\n\n\t\t\tforeach(idx; 0 .. nfds) {\n\t\t\t\tversion(linux) {\n\t\t\t\t\tauto flags = events[idx].events;\n\t\t\t\t\tauto ioop = cast(IoOp*) events[idx].data.ptr;\n\t\t\t\t} else {\n\t\t\t\t\tIoOp* ioop;\n\t\t\t\t\tforeach(tidx, thing; pollfds[lastIdx .. $]) {\n\t\t\t\t\t\tif(thing.revents) {\n\t\t\t\t\t\t\tioop = ioops[thing.fd];\n\t\t\t\t\t\t\tlastIdx += tidx + 1;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t//writeln(flags, \" \", ioop.fd);\n\n\t\t\t\tvoid newConnection() {\n\t\t\t\t\t// on edge triggering, it is important that we get it all\n\t\t\t\t\twhile(true) {\n\t\t\t\t\t\tversion(Android) {\n\t\t\t\t\t\t\tauto size = cast(int) addr.sizeof;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tauto size = cast(uint) addr.sizeof;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tauto ns = accept(sock, cast(sockaddr*) &addr, &size);\n\t\t\t\t\t\tif(ns == -1) {\n\t\t\t\t\t\t\tif(errno == EAGAIN || errno == EWOULDBLOCK) {\n\t\t\t\t\t\t\t\t// all done, got it all\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tthrow new Exception(\"accept \" ~ to!string(errno));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcloexec(ns);\n\n\t\t\t\t\t\tmakeNonBlocking(ns);\n\t\t\t\t\t\tauto niop = allocateIoOp(ns, IoOp.ReadSocketHandle, 4096 * 4, &eis.handleLocalConnectionData);\n\t\t\t\t\t\tniop.closeHandler = &eis.handleLocalConnectionClose;\n\t\t\t\t\t\tniop.completeHandler = &eis.handleLocalConnectionComplete;\n\t\t\t\t\t\tscope(failure) freeIoOp(niop);\n\n\t\t\t\t\t\tversion(linux) {\n\t\t\t\t\t\t\tepoll_event nev;\n\t\t\t\t\t\t\tnev.events = EPOLLIN | EPOLLET;\n\t\t\t\t\t\t\tnev.data.ptr = niop;\n\t\t\t\t\t\t\tif(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ns, &nev) == -1)\n\t\t\t\t\t\t\t\tthrow new Exception(\"epoll_ctl \" ~ to!string(errno));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbool found = false;\n\t\t\t\t\t\t\tforeach(ref pfd; pollfds) {\n\t\t\t\t\t\t\t\tif(pfd.fd < 0) {\n\t\t\t\t\t\t\t\t\tpfd.fd = ns;\n\t\t\t\t\t\t\t\t\tfound = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(!found)\n\t\t\t\t\t\t\t\tpollfds ~= pollfd(ns, POLLIN);\n\t\t\t\t\t\t\tioops[ns] = niop;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tbool newConnectionCondition() {\n\t\t\t\t\tversion(linux)\n\t\t\t\t\t\treturn ioop.fd == sock && (flags & EPOLLIN);\n\t\t\t\t\telse\n\t\t\t\t\t\treturn pollfds[idx].fd == sock && (pollfds[idx].revents & POLLIN);\n\t\t\t\t}\n\n\t\t\t\tif(newConnectionCondition()) {\n\t\t\t\t\tnewConnection();\n\t\t\t\t} else if(ioop.operation == IoOp.ReadSocketHandle) {\n\t\t\t\t\twhile(true) {\n\t\t\t\t\t\tint in_fd;\n\t\t\t\t\t\tauto got = read_fd(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length, &in_fd);\n\t\t\t\t\t\tif(got == -1) {\n\t\t\t\t\t\t\tif(errno == EAGAIN || errno == EWOULDBLOCK) {\n\t\t\t\t\t\t\t\t// all done, got it all\n\t\t\t\t\t\t\t\tif(ioop.completeHandler)\n\t\t\t\t\t\t\t\t\tioop.completeHandler(ioop);\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tthrow new Exception(\"recv \" ~ to!string(errno));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif(got == 0) {\n\t\t\t\t\t\t\tif(ioop.closeHandler) {\n\t\t\t\t\t\t\t\tioop.closeHandler(ioop);\n\t\t\t\t\t\t\t\tversion(linux) {} // nothing needed\n\t\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\t\tforeach(ref pfd; pollfds) {\n\t\t\t\t\t\t\t\t\t\tif(pfd.fd == ioop.fd)\n\t\t\t\t\t\t\t\t\t\t\tpfd.fd = -1;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tclose(ioop.fd);\n\t\t\t\t\t\t\tfreeIoOp(ioop);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tioop.bufferLengthUsed = cast(int) got;\n\t\t\t\t\t\tioop.handler(ioop, in_fd);\n\t\t\t\t\t}\n\t\t\t\t} else if(ioop.operation == IoOp.Read) {\n\t\t\t\t\twhile(true) {\n\t\t\t\t\t\tauto got = read(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length);\n\t\t\t\t\t\tif(got == -1) {\n\t\t\t\t\t\t\tif(errno == EAGAIN || errno == EWOULDBLOCK) {\n\t\t\t\t\t\t\t\t// all done, got it all\n\t\t\t\t\t\t\t\tif(ioop.completeHandler)\n\t\t\t\t\t\t\t\t\tioop.completeHandler(ioop);\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tthrow new Exception(\"recv \" ~ to!string(ioop.fd) ~ \" errno \" ~ to!string(errno));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif(got == 0) {\n\t\t\t\t\t\t\tif(ioop.closeHandler)\n\t\t\t\t\t\t\t\tioop.closeHandler(ioop);\n\t\t\t\t\t\t\tclose(ioop.fd);\n\t\t\t\t\t\t\tfreeIoOp(ioop);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tioop.bufferLengthUsed = cast(int) got;\n\t\t\t\t\t\tif(ioop.handler(ioop, ioop.fd)) {\n\t\t\t\t\t\t\tclose(ioop.fd);\n\t\t\t\t\t\t\tfreeIoOp(ioop);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// EPOLLHUP?\n\t\t\t}\n\t\t}\n\t} else version(Windows) {\n\n\t\t// set up a named pipe\n\t\t// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx\n\t\t// https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsaduplicatesocketw\n\t\t// https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-getnamedpipeserverprocessid\n\n\t} else static assert(0);\n}\n\n\nversion(with_sendfd)\n// copied from the web and ported from C\n// see https://stackoverflow.com/questions/2358684/can-i-share-a-file-descriptor-to-another-process-on-linux-or-are-they-local-to-t\nssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) {\n\tmsghdr msg;\n\tiovec[1] iov;\n\n\tversion(OSX) {\n\t\t// removed\n\t} else version(Android) {\n\t} else {\n\t\tunion ControlUnion {\n\t\t\tcmsghdr cm;\n\t\t\tchar[CMSG_SPACE(int.sizeof)] control;\n\t\t}\n\n\t\tControlUnion control_un;\n\t\tcmsghdr* cmptr;\n\n\t\tmsg.msg_control = control_un.control.ptr;\n\t\tmsg.msg_controllen = control_un.control.length;\n\n\t\tcmptr = CMSG_FIRSTHDR(&msg);\n\t\tcmptr.cmsg_len = CMSG_LEN(int.sizeof);\n\t\tcmptr.cmsg_level = SOL_SOCKET;\n\t\tcmptr.cmsg_type = SCM_RIGHTS;\n\t\t*(cast(int *) CMSG_DATA(cmptr)) = sendfd;\n\t}\n\n\tmsg.msg_name = null;\n\tmsg.msg_namelen = 0;\n\n\tiov[0].iov_base = ptr;\n\tiov[0].iov_len = nbytes;\n\tmsg.msg_iov = iov.ptr;\n\tmsg.msg_iovlen = 1;\n\n\treturn sendmsg(fd, &msg, 0);\n}\n\nversion(with_sendfd)\n// copied from the web and ported from C\nssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) {\n\tmsghdr msg;\n\tiovec[1] iov;\n\tssize_t n;\n\tint newfd;\n\n\tversion(OSX) {\n\t\t//msg.msg_accrights = cast(cattr_t) recvfd;\n\t\t//msg.msg_accrightslen = int.sizeof;\n\t} else version(Android) {\n\t} else {\n\t\tunion ControlUnion {\n\t\t\tcmsghdr cm;\n\t\t\tchar[CMSG_SPACE(int.sizeof)] control;\n\t\t}\n\t\tControlUnion control_un;\n\t\tcmsghdr* cmptr;\n\n\t\tmsg.msg_control = control_un.control.ptr;\n\t\tmsg.msg_controllen = control_un.control.length;\n\t}\n\n\tmsg.msg_name = null;\n\tmsg.msg_namelen = 0;\n\n\tiov[0].iov_base = ptr;\n\tiov[0].iov_len = nbytes;\n\tmsg.msg_iov = iov.ptr;\n\tmsg.msg_iovlen = 1;\n\n\tif ( (n = recvmsg(fd, &msg, 0)) <= 0)\n\t\treturn n;\n\n\tversion(OSX) {\n\t\t//if(msg.msg_accrightslen != int.sizeof)\n\t\t\t//*recvfd = -1;\n\t} else version(Android) {\n\t} else {\n\t\tif ( (cmptr = CMSG_FIRSTHDR(&msg)) != null &&\n\t\t\t\tcmptr.cmsg_len == CMSG_LEN(int.sizeof)) {\n\t\t\tif (cmptr.cmsg_level != SOL_SOCKET)\n\t\t\t\tthrow new Exception(\"control level != SOL_SOCKET\");\n\t\t\tif (cmptr.cmsg_type != SCM_RIGHTS)\n\t\t\t\tthrow new Exception(\"control type != SCM_RIGHTS\");\n\t\t\t*recvfd = *(cast(int *) CMSG_DATA(cmptr));\n\t\t} else\n\t\t\t*recvfd = -1;       /* descriptor was not passed */\n\t}\n\n\treturn n;\n}\n/* end read_fd */\n\n\n/*\n\tEvent source stuff\n\n\tThe api is:\n\n\tsendEvent(string url, string type, string data, int timeout = 60*10);\n\n\tattachEventListener(string url, int fd, lastId)\n\n\n\tIt just sends to all attached listeners, and stores it until the timeout\n\tfor replaying via lastEventId.\n*/\n\n/*\n\tSession process stuff\n\n\tit stores it all. the cgi object has a session object that can grab it\n\n\tsession may be done in the same process if possible, there is a version\n\tswitch to choose if you want to override.\n*/\n\nstruct DispatcherDefinition(alias dispatchHandler, DispatcherDetails = typeof(null)) {// if(is(typeof(dispatchHandler(\"str\", Cgi.init, void) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler;\n\talias handler = dispatchHandler;\n\tstring urlPrefix;\n\tbool rejectFurther;\n\timmutable(DispatcherDetails) details;\n}\n\nprivate string urlify(string name) pure {\n\treturn beautify(name, '-', true);\n}\n\nprivate string beautify(string name, char space = ' ', bool allLowerCase = false) pure {\n\tif(name == \"id\")\n\t\treturn allLowerCase ? name : \"ID\";\n\n\tchar[160] buffer;\n\tint bufferIndex = 0;\n\tbool shouldCap = true;\n\tbool shouldSpace;\n\tbool lastWasCap;\n\tforeach(idx, char ch; name) {\n\t\tif(bufferIndex == buffer.length) return name; // out of space, just give up, not that important\n\n\t\tif((ch >= 'A' && ch <= 'Z') || ch == '_') {\n\t\t\tif(lastWasCap) {\n\t\t\t\t// two caps in a row, don't change. Prolly acronym.\n\t\t\t} else {\n\t\t\t\tif(idx)\n\t\t\t\t\tshouldSpace = true; // new word, add space\n\t\t\t}\n\n\t\t\tlastWasCap = true;\n\t\t} else {\n\t\t\tlastWasCap = false;\n\t\t}\n\n\t\tif(shouldSpace) {\n\t\t\tbuffer[bufferIndex++] = space;\n\t\t\tif(bufferIndex == buffer.length) return name; // out of space, just give up, not that important\n\t\t\tshouldSpace = false;\n\t\t}\n\t\tif(shouldCap) {\n\t\t\tif(ch >= 'a' && ch <= 'z')\n\t\t\t\tch -= 32;\n\t\t\tshouldCap = false;\n\t\t}\n\t\tif(allLowerCase && ch >= 'A' && ch <= 'Z')\n\t\t\tch += 32;\n\t\tbuffer[bufferIndex++] = ch;\n\t}\n\treturn buffer[0 .. bufferIndex].idup;\n}\n\n/*\nstring urlFor(alias func)() {\n\treturn __traits(identifier, func);\n}\n*/\n\n/++\n\tUDA: The name displayed to the user in auto-generated HTML.\n\n\tDefault is `beautify(identifier)`.\n+/\nstruct DisplayName {\n\tstring name;\n}\n\n/++\n\tUDA: The name used in the URL or web parameter.\n\n\tDefault is `urlify(identifier)` for functions and `identifier` for parameters and data members.\n+/\nstruct UrlName {\n\tstring name;\n}\n\n/++\n\tUDA: default format to respond for this method\n+/\nstruct DefaultFormat { string value; }\n\nclass MissingArgumentException : Exception {\n\tstring functionName;\n\tstring argumentName;\n\tstring argumentType;\n\n\tthis(string functionName, string argumentName, string argumentType, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {\n\t\tthis.functionName = functionName;\n\t\tthis.argumentName = argumentName;\n\t\tthis.argumentType = argumentType;\n\n\t\tsuper(\"Missing Argument: \" ~ this.argumentName, file, line, next);\n\t}\n}\n\n/++\n\tYou can throw this from an api handler to indicate a 404 response. This is done by the presentExceptionAsHtml function in the presenter.\n\n\tHistory:\n\t\tAdded December 15, 2021 (dub v10.5)\n+/\nclass ResourceNotFoundException : Exception {\n\tstring resourceType;\n\tstring resourceId;\n\n\tthis(string resourceType, string resourceId, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {\n\t\tthis.resourceType = resourceType;\n\t\tthis.resourceId = resourceId;\n\n\t\tsuper(\"Resource not found: \" ~ resourceType ~ \" \" ~ resourceId, file, line, next);\n\t}\n\n}\n\n/++\n\tThis can be attached to any constructor or function called from the cgi system.\n\n\tIf it is present, the function argument can NOT be set from web params, but instead\n\tis set to the return value of the given `func`.\n\n\tIf `func` can take a parameter of type [Cgi], it will be passed the one representing\n\tthe current request. Otherwise, it must take zero arguments.\n\n\tAny params in your function of type `Cgi` are automatically assumed to take the cgi object\n\tfor the connection. Any of type [Session] (with an argument) is\talso assumed to come from\n\tthe cgi object.\n\n\tconst arguments are also supported.\n+/\nstruct ifCalledFromWeb(alias func) {}\n\n// it only looks at query params for GET requests, the rest must be in the body for a function argument.\nauto callFromCgi(alias method, T)(T dg, Cgi cgi) {\n\n\t// FIXME: any array of structs should also be settable or gettable from csv as well.\n\n\t// FIXME: think more about checkboxes and bools.\n\n\timport std.traits;\n\n\tParameters!method params;\n\talias idents = ParameterIdentifierTuple!method;\n\talias defaults = ParameterDefaults!method;\n\n\tconst(string)[] names;\n\tconst(string)[] values;\n\n\t// first, check for missing arguments and initialize to defaults if necessary\n\n\tstatic if(is(typeof(method) P == __parameters))\n\tforeach(idx, param; P) {{\n\t\t// see: mustNotBeSetFromWebParams\n\t\tstatic if(is(param : Cgi)) {\n\t\t\tstatic assert(!is(param == immutable));\n\t\t\tcast() params[idx] = cgi;\n\t\t} else static if(is(param == Session!D, D)) {\n\t\t\tstatic assert(!is(param == immutable));\n\t\t\tcast() params[idx] = cgi.getSessionObject!D();\n\t\t} else {\n\t\t\tbool populated;\n\t\t\tforeach(uda; __traits(getAttributes, P[idx .. idx + 1])) {\n\t\t\t\tstatic if(is(uda == ifCalledFromWeb!func, alias func)) {\n\t\t\t\t\tstatic if(is(typeof(func(cgi))))\n\t\t\t\t\t\tparams[idx] = func(cgi);\n\t\t\t\t\telse\n\t\t\t\t\t\tparams[idx] = func();\n\n\t\t\t\t\tpopulated = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif(!populated) {\n\t\t\t\tstatic if(__traits(compiles, { params[idx] = param.getAutomaticallyForCgi(cgi); } )) {\n\t\t\t\t\tparams[idx] = param.getAutomaticallyForCgi(cgi);\n\t\t\t\t\tpopulated = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif(!populated) {\n\t\t\t\tauto ident = idents[idx];\n\t\t\t\tif(cgi.requestMethod == Cgi.RequestMethod.GET) {\n\t\t\t\t\tif(ident !in cgi.get) {\n\t\t\t\t\t\tstatic if(is(defaults[idx] == void)) {\n\t\t\t\t\t\t\tstatic if(is(param == bool))\n\t\t\t\t\t\t\t\tparams[idx] = false;\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\tthrow new MissingArgumentException(__traits(identifier, method), ident, param.stringof);\n\t\t\t\t\t\t} else\n\t\t\t\t\t\t\tparams[idx] = defaults[idx];\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif(ident !in cgi.post) {\n\t\t\t\t\t\tstatic if(is(defaults[idx] == void)) {\n\t\t\t\t\t\t\tstatic if(is(param == bool))\n\t\t\t\t\t\t\t\tparams[idx] = false;\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\tthrow new MissingArgumentException(__traits(identifier, method), ident, param.stringof);\n\t\t\t\t\t\t} else\n\t\t\t\t\t\t\tparams[idx] = defaults[idx];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}}\n\n\t// second, parse the arguments in order to build up arrays, etc.\n\n\tstatic bool setVariable(T)(string name, string paramName, T* what, string value) {\n\t\tstatic if(is(T == struct)) {\n\t\t\tif(name == paramName) {\n\t\t\t\t*what = T.init;\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\t// could be a child. gonna allow either obj.field OR obj[field]\n\n\t\t\t\tstring afterName;\n\n\t\t\t\tif(name[paramName.length] == '[') {\n\t\t\t\t\tint count = 1;\n\t\t\t\t\tauto idx = paramName.length + 1;\n\t\t\t\t\twhile(idx < name.length && count > 0) {\n\t\t\t\t\t\tif(name[idx] == '[')\n\t\t\t\t\t\t\tcount++;\n\t\t\t\t\t\telse if(name[idx] == ']') {\n\t\t\t\t\t\t\tcount--;\n\t\t\t\t\t\t\tif(count == 0) break;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidx++;\n\t\t\t\t\t}\n\n\t\t\t\t\tif(idx == name.length)\n\t\t\t\t\t\treturn false; // malformed\n\n\t\t\t\t\tauto insideBrackets = name[paramName.length + 1 .. idx];\n\t\t\t\t\tafterName = name[idx + 1 .. $];\n\n\t\t\t\t\tname = name[0 .. paramName.length];\n\n\t\t\t\t\tparamName = insideBrackets;\n\n\t\t\t\t} else if(name[paramName.length] == '.') {\n\t\t\t\t\tparamName = name[paramName.length + 1 .. $];\n\t\t\t\t\tname = paramName;\n\t\t\t\t\tint p = 0;\n\t\t\t\t\tforeach(ch; paramName) {\n\t\t\t\t\t\tif(ch == '.' || ch == '[')\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tp++;\n\t\t\t\t\t}\n\n\t\t\t\t\tafterName = paramName[p .. $];\n\t\t\t\t\tparamName = paramName[0 .. p];\n\t\t\t\t} else {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif(paramName.length)\n\t\t\t\t// set the child member\n\t\t\t\tswitch(paramName) {\n\t\t\t\t\tforeach(idx, memberName; __traits(allMembers, T))\n\t\t\t\t\tstatic if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {\n\t\t\t\t\t\t// data member!\n\t\t\t\t\t\tcase memberName:\n\t\t\t\t\t\t\treturn setVariable(name ~ afterName, paramName, &(__traits(getMember, *what, memberName)), value);\n\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t// ok, not a member\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn false;\n\t\t} else static if(is(T == enum)) {\n\t\t\t*what = to!T(value);\n\t\t\treturn true;\n\t\t} else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) {\n\t\t\t*what = to!T(value);\n\t\t\treturn true;\n\t\t} else static if(is(T == bool)) {\n\t\t\t*what = value == \"1\" || value == \"yes\" || value == \"t\" || value == \"true\" || value == \"on\";\n\t\t\treturn true;\n\t\t} else static if(is(T == K[], K)) {\n\t\t\tK tmp;\n\t\t\tif(name == paramName) {\n\t\t\t\t// direct - set and append\n\t\t\t\tif(setVariable(name, paramName, &tmp, value)) {\n\t\t\t\t\t(*what) ~= tmp;\n\t\t\t\t\treturn true;\n\t\t\t\t} else {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// child, append to last element\n\t\t\t\t// FIXME: what about range violations???\n\t\t\t\tauto ptr = &(*what)[(*what).length - 1];\n\t\t\t\treturn setVariable(name, paramName, ptr, value);\n\n\t\t\t}\n\t\t} else static if(is(T == V[K], K, V)) {\n\t\t\t// assoc array, name[key] is valid\n\t\t\tif(name == paramName) {\n\t\t\t\t// no action necessary\n\t\t\t\treturn true;\n\t\t\t} else if(name[paramName.length] == '[') {\n\t\t\t\tint count = 1;\n\t\t\t\tauto idx = paramName.length + 1;\n\t\t\t\twhile(idx < name.length && count > 0) {\n\t\t\t\t\tif(name[idx] == '[')\n\t\t\t\t\t\tcount++;\n\t\t\t\t\telse if(name[idx] == ']') {\n\t\t\t\t\t\tcount--;\n\t\t\t\t\t\tif(count == 0) break;\n\t\t\t\t\t}\n\t\t\t\t\tidx++;\n\t\t\t\t}\n\t\t\t\tif(idx == name.length)\n\t\t\t\t\treturn false; // malformed\n\n\t\t\t\tauto insideBrackets = name[paramName.length + 1 .. idx];\n\t\t\t\tauto afterName = name[idx + 1 .. $];\n\n\t\t\t\tauto k = to!K(insideBrackets);\n\t\t\t\tV v;\n\t\t\t\tif(auto ptr = k in *what)\n\t\t\t\t\tv = *ptr;\n\n\t\t\t\tname = name[0 .. paramName.length];\n\t\t\t\t//writeln(name, afterName, \" \", paramName);\n\n\t\t\t\tauto ret = setVariable(name ~ afterName, paramName, &v, value);\n\t\t\t\tif(ret) {\n\t\t\t\t\t(*what)[k] = v;\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn false;\n\t\t} else {\n\t\t\tstatic assert(0, \"unsupported type for cgi call \" ~ T.stringof);\n\t\t}\n\n\t\t//return false;\n\t}\n\n\tvoid setArgument(string name, string value) {\n\t\tint p;\n\t\tforeach(ch; name) {\n\t\t\tif(ch == '.' || ch == '[')\n\t\t\t\tbreak;\n\t\t\tp++;\n\t\t}\n\n\t\tauto paramName = name[0 .. p];\n\n\t\tsw: switch(paramName) {\n\t\t\tstatic if(is(typeof(method) P == __parameters))\n\t\t\tforeach(idx, param; P) {\n\t\t\t\tstatic if(mustNotBeSetFromWebParams!(P[idx], __traits(getAttributes, P[idx .. idx + 1]))) {\n\t\t\t\t\t// cannot be set from the outside\n\t\t\t\t} else {\n\t\t\t\t\tcase idents[idx]:\n\t\t\t\t\t\tstatic if(is(param == Cgi.UploadedFile)) {\n\t\t\t\t\t\t\tparams[idx] = cgi.files[name];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsetVariable(name, paramName, &params[idx], value);\n\t\t\t\t\t\t}\n\t\t\t\t\tbreak sw;\n\t\t\t\t}\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// ignore; not relevant argument\n\t\t}\n\t}\n\n\tif(cgi.requestMethod == Cgi.RequestMethod.GET) {\n\t\tnames = cgi.allGetNamesInOrder;\n\t\tvalues = cgi.allGetValuesInOrder;\n\t} else {\n\t\tnames = cgi.allPostNamesInOrder;\n\t\tvalues = cgi.allPostValuesInOrder;\n\t}\n\n\tforeach(idx, name; names) {\n\t\tsetArgument(name, values[idx]);\n\t}\n\n\tstatic if(is(ReturnType!method == void)) {\n\t\ttypeof(null) ret;\n\t\tdg(params);\n\t} else {\n\t\tauto ret = dg(params);\n\t}\n\n\t// FIXME: format return values\n\t// options are: json, html, csv.\n\t// also may need to wrap in envelope format: none, html, or json.\n\treturn ret;\n}\n\nprivate bool mustNotBeSetFromWebParams(T, attrs...)() {\n\tstatic if(is(T : const(Cgi))) {\n\t\treturn true;\n\t} else static if(is(T : const(Session!D), D)) {\n\t\treturn true;\n\t} else static if(__traits(compiles, T.getAutomaticallyForCgi(Cgi.init))) {\n\t\treturn true;\n\t} else {\n\t\tforeach(uda; attrs)\n\t\t\tstatic if(is(uda == ifCalledFromWeb!func, alias func))\n\t\t\t\treturn true;\n\t\treturn false;\n\t}\n}\n\nprivate bool hasIfCalledFromWeb(attrs...)() {\n\tforeach(uda; attrs)\n\t\tstatic if(is(uda == ifCalledFromWeb!func, alias func))\n\t\t\treturn true;\n\treturn false;\n}\n\n/++\n\tImplies POST path for the thing itself, then GET will get the automatic form.\n\n\tThe given customizer, if present, will be called as a filter on the Form object.\n\n\tHistory:\n\t\tAdded December 27, 2020\n+/\ntemplate AutomaticForm(alias customizer) { }\n\n/++\n\tThis is meant to be returned by a function that takes a form POST submission. You\n\twant to set the url of the new resource it created, which is set as the http\n\tLocation header for a \"201 Created\" result, and you can also set a separate\n\tdestination for browser users, which it sets via a \"Refresh\" header.\n\n\tThe `resourceRepresentation` should generally be the thing you just created, and\n\tit will be the body of the http response when formatted through the presenter.\n\tThe exact thing is up to you - it could just return an id, or the whole object, or\n\tperhaps a partial object.\n\n\tExamples:\n\t---\n\tclass Test : WebObject {\n\t\t@(Cgi.RequestMethod.POST)\n\t\tCreatedResource!int makeThing(string value) {\n\t\t\treturn CreatedResource!int(value.to!int, \"/resources/id\");\n\t\t}\n\t}\n\t---\n\n\tHistory:\n\t\tAdded December 18, 2021\n+/\nstruct CreatedResource(T) {\n\tstatic if(!is(T == void))\n\t\tT resourceRepresentation;\n\tstring resourceUrl;\n\tstring refreshUrl;\n}\n\n/+\n/++\n\tThis can be attached as a UDA to a handler to add a http Refresh header on a\n\tsuccessful run. (It will not be attached if the function throws an exception.)\n\tThis will refresh the browser the given number of seconds after the page loads,\n\tto the url returned by `urlFunc`, which can be either a static function or a\n\tmember method of the current handler object.\n\n\tYou might use this for a POST handler that is normally used from ajax, but you\n\twant it to degrade gracefully to a temporarily flashed message before reloading\n\tthe main page.\n\n\tHistory:\n\t\tAdded December 18, 2021\n+/\nstruct Refresh(alias urlFunc) {\n\tint waitInSeconds;\n\n\tstring url() {\n\t\tstatic if(__traits(isStaticFunction, urlFunc))\n\t\t\treturn urlFunc();\n\t\telse static if(is(urlFunc : string))\n\t\t\treturn urlFunc;\n\t}\n}\n+/\n\n/+\n/++\n\tSets a filter to be run before\n\n\tA before function can do validations of params and log and stop the function from running.\n+/\ntemplate Before(alias b) {}\ntemplate After(alias b) {}\n+/\n\n/+\n\tArgument conversions: for the most part, it is to!Thing(string).\n\n\tBut arrays and structs are a bit different. Arrays come from the cgi array. Thus\n\tthey are passed\n\n\tarr=foo&arr=bar <-- notice the same name.\n\n\tStructs are first declared with an empty thing, then have their members set individually,\n\twith dot notation. The members are not required, just the initial declaration.\n\n\tstruct Foo {\n\t\tint a;\n\t\tstring b;\n\t}\n\tvoid test(Foo foo){}\n\n\tfoo&foo.a=5&foo.b=str <-- the first foo declares the arg, the others set the members\n\n\tArrays of structs use this declaration.\n\n\tvoid test(Foo[] foo) {}\n\n\tfoo&foo.a=5&foo.b=bar&foo&foo.a=9\n\n\tYou can use a hidden input field in HTML forms to achieve this. The value of the naked name\n\tdeclaration is ignored.\n\n\tMind that order matters! The declaration MUST come first in the string.\n\n\tArrays of struct members follow this rule recursively.\n\n\tstruct Foo {\n\t\tint[] a;\n\t}\n\n\tfoo&foo.a=1&foo.a=2&foo&foo.a=1\n\n\n\tAssociative arrays are formatted with brackets, after a declaration, like structs:\n\n\tfoo&foo[key]=value&foo[other_key]=value\n\n\n\tNote: for maximum compatibility with outside code, keep your types simple. Some libraries\n\tdo not support the strict ordering requirements to work with these struct protocols.\n\n\tFIXME: also perhaps accept application/json to better work with outside trash.\n\n\n\tReturn values are also auto-formatted according to user-requested type:\n\t\tfor json, it loops over and converts.\n\t\tfor html, basic types are strings. Arrays are <ol>. Structs are <dl>. Arrays of structs are tables!\n+/\n\n/++\n\tA web presenter is responsible for rendering things to HTML to be usable\n\tin a web browser.\n\n\tThey are passed as template arguments to the base classes of [WebObject]\n\n\tResponsible for displaying stuff as HTML. You can put this into your own aggregate\n\tand override it. Use forwarding and specialization to customize it.\n\n\tWhen you inherit from it, pass your own class as the CRTP argument. This lets the base\n\tclass templates and your overridden templates work with each other.\n\n\t---\n\tclass MyPresenter : WebPresenter!(MyPresenter) {\n\t\t@Override\n\t\tvoid presentSuccessfulReturnAsHtml(T : CustomType)(Cgi cgi, T ret, typeof(null) meta) {\n\t\t\t// present the CustomType\n\t\t}\n\t\t@Override\n\t\tvoid presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) {\n\t\t\t// handle everything else via the super class, which will call\n\t\t\t// back to your class when appropriate\n\t\t\tsuper.presentSuccessfulReturnAsHtml(cgi, ret);\n\t\t}\n\t}\n\t---\n\n\tThe meta argument in there can be overridden by your own facility.\n\n+/\nclass WebPresenter(CRTP) {\n\n\t/// A UDA version of the built-in `override`, to be used for static template polymorphism\n\t/// If you override a plain method, use `override`. If a template, use `@Override`.\n\tenum Override;\n\n\tstring script() {\n\t\treturn `\n\t\t`;\n\t}\n\n\tstring style() {\n\t\treturn `\n\t\t\t:root {\n\t\t\t\t--mild-border: #ccc;\n\t\t\t\t--middle-border: #999;\n\t\t\t\t--accent-color: #f2f2f2;\n\t\t\t\t--sidebar-color: #fefefe;\n\t\t\t}\n\t\t` ~ genericFormStyling() ~ genericSiteStyling();\n\t}\n\n\tstring genericFormStyling() {\n\t\treturn\nq\"css\n\t\t\ttable.automatic-data-display {\n\t\t\t\tborder-collapse: collapse;\n\t\t\t\tborder: solid 1px var(--mild-border);\n\t\t\t}\n\n\t\t\ttable.automatic-data-display td {\n\t\t\t\tvertical-align: top;\n\t\t\t\tborder: solid 1px var(--mild-border);\n\t\t\t\tpadding: 2px 4px;\n\t\t\t}\n\n\t\t\ttable.automatic-data-display th {\n\t\t\t\tborder: solid 1px var(--mild-border);\n\t\t\t\tborder-bottom: solid 1px var(--middle-border);\n\t\t\t\tpadding: 2px 4px;\n\t\t\t}\n\n\t\t\tol.automatic-data-display {\n\t\t\t\tmargin: 0px;\n\t\t\t\tlist-style-position: inside;\n\t\t\t\tpadding: 0px;\n\t\t\t}\n\n\t\t\tdl.automatic-data-display {\n\n\t\t\t}\n\n\t\t\t.automatic-form {\n\t\t\t\tmax-width: 600px;\n\t\t\t}\n\n\t\t\t.form-field {\n\t\t\t\tmargin: 0.5em;\n\t\t\t\tpadding-left: 0.5em;\n\t\t\t}\n\n\t\t\t.label-text {\n\t\t\t\tdisplay: block;\n\t\t\t\tfont-weight: bold;\n\t\t\t\tmargin-left: -0.5em;\n\t\t\t}\n\n\t\t\t.submit-button-holder {\n\t\t\t\tpadding-left: 2em;\n\t\t\t}\n\n\t\t\t.add-array-button {\n\n\t\t\t}\ncss\";\n\t}\n\n\tstring genericSiteStyling() {\n\t\treturn\nq\"css\n\t\t\t* { box-sizing: border-box; }\n\t\t\thtml, body { margin: 0px; }\n\t\t\tbody {\n\t\t\t\tfont-family: sans-serif;\n\t\t\t}\n\t\t\theader {\n\t\t\t\tbackground: var(--accent-color);\n\t\t\t\theight: 64px;\n\t\t\t}\n\t\t\tfooter {\n\t\t\t\tbackground: var(--accent-color);\n\t\t\t\theight: 64px;\n\t\t\t}\n\t\t\t#site-container {\n\t\t\t\tdisplay: flex;\n\t\t\t}\n\t\t\tmain {\n\t\t\t\tflex: 1 1 auto;\n\t\t\t\torder: 2;\n\t\t\t\tmin-height: calc(100vh - 64px - 64px);\n\t\t\t\tpadding: 4px;\n\t\t\t\tpadding-left: 1em;\n\t\t\t}\n\t\t\t#sidebar {\n\t\t\t\tflex: 0 0 16em;\n\t\t\t\torder: 1;\n\t\t\t\tbackground: var(--sidebar-color);\n\t\t\t}\ncss\";\n\t}\n\n\timport arsd.dom;\n\tElement htmlContainer() {\n\t\tauto document = new Document(q\"html\n<!DOCTYPE html>\n<html class=\"no-script\">\n<head>\n\t<script>document.documentElement.classList.remove(\"no-script\");</script>\n\t<style>.no-script requires-script { display: none; }</style>\n\t<title>D Application</title>\n\t<link rel=\"stylesheet\" href=\"style.css\" />\n</head>\n<body>\n\t<header></header>\n\t<div id=\"site-container\">\n\t\t<main></main>\n\t\t<div id=\"sidebar\"></div>\n\t</div>\n\t<footer></footer>\n\t<script src=\"script.js\"></script>\n</body>\n</html>\nhtml\", true, true);\n\n\t\treturn document.requireSelector(\"main\");\n\t}\n\n\t/// Renders a response as an HTTP error\n\tvoid renderBasicError(Cgi cgi, int httpErrorCode) {\n\t\tcgi.setResponseStatus(getHttpCodeText(httpErrorCode));\n\t\tauto c = htmlContainer();\n\t\tc.innerText = getHttpCodeText(httpErrorCode);\n\t\tcgi.setResponseContentType(\"text/html; charset=utf-8\");\n\t\tcgi.write(c.parentDocument.toString(), true);\n\t}\n\n\ttemplate methodMeta(alias method) {\n\t\tenum methodMeta = null;\n\t}\n\n\tvoid presentSuccessfulReturn(T, Meta)(Cgi cgi, T ret, Meta meta, string format) {\n\t\tswitch(format) {\n\t\t\tcase \"html\":\n\t\t\t\t(cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret, meta);\n\t\t\tbreak;\n\t\t\tcase \"json\":\n\t\t\t\timport arsd.jsvar;\n\t\t\t\tstatic if(is(typeof(ret) == MultipleResponses!Types, Types...)) {\n\t\t\t\t\tvar json;\n\t\t\t\t\tforeach(index, type; Types) {\n\t\t\t\t\t\tif(ret.contains == index)\n\t\t\t\t\t\t\tjson = ret.payload[index];\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tvar json = ret;\n\t\t\t\t}\n\t\t\t\tvar envelope = json; // var.emptyObject;\n\t\t\t\t/*\n\t\t\t\tenvelope.success = true;\n\t\t\t\tenvelope.result = json;\n\t\t\t\tenvelope.error = null;\n\t\t\t\t*/\n\t\t\t\tcgi.setResponseContentType(\"application/json\");\n\t\t\t\tcgi.write(envelope.toJson(), true);\n\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tcgi.setResponseStatus(\"406 Not Acceptable\"); // not exactly but sort of.\n\t\t}\n\t}\n\n\t/// typeof(null) (which is also used to represent functions returning `void`) do nothing\n\t/// in the default presenter - allowing the function to have full low-level control over the\n\t/// response.\n\tvoid presentSuccessfulReturn(T : typeof(null), Meta)(Cgi cgi, T ret, Meta meta, string format) {\n\t\t// nothing intentionally!\n\t}\n\n\t/// Redirections are forwarded to [Cgi.setResponseLocation]\n\tvoid presentSuccessfulReturn(T : Redirection, Meta)(Cgi cgi, T ret, Meta meta, string format) {\n\t\tcgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code));\n\t}\n\n\t/// [CreatedResource]s send code 201 and will set the given urls, then present the given representation.\n\tvoid presentSuccessfulReturn(T : CreatedResource!R, Meta, R)(Cgi cgi, T ret, Meta meta, string format) {\n\t\tcgi.setResponseStatus(getHttpCodeText(201));\n\t\tif(ret.resourceUrl.length)\n\t\t\tcgi.header(\"Location: \" ~ ret.resourceUrl);\n\t\tif(ret.refreshUrl.length)\n\t\t\tcgi.header(\"Refresh: 0;\" ~ ret.refreshUrl);\n\t\tstatic if(!is(R == void))\n\t\t\tpresentSuccessfulReturn(cgi, ret.resourceRepresentation, meta, format);\n\t}\n\n\t/// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime\n\tvoid presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) {\n\t\tbool outputted = false;\n\t\tforeach(index, type; Types) {\n\t\t\tif(ret.contains == index) {\n\t\t\t\tassert(!outputted);\n\t\t\t\toutputted = true;\n\t\t\t\t(cast(CRTP) this).presentSuccessfulReturn(cgi, ret.payload[index], meta, format);\n\t\t\t}\n\t\t}\n\t\tif(!outputted)\n\t\t\tassert(0);\n\t}\n\n\t/++\n\t\tAn 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.\n\t+/\n\tvoid presentSuccessfulReturn(T : FileResource, Meta)(Cgi cgi, T ret, Meta meta, string format) {\n\t\tcgi.setCache(true); // not necessarily true but meh\n\t\tif(auto fn = ret.filename()) {\n\t\t\tcgi.header(\"Content-Disposition: attachment; filename=\"~fn~\";\");\n\t\t}\n\t\tcgi.setResponseContentType(ret.contentType);\n\t\tcgi.write(ret.getData(), true);\n\t}\n\n\t/// And the default handler for HTML will call [formatReturnValueAsHtml] and place it inside the [htmlContainer].\n\tvoid presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) {\n\t\tauto container = this.htmlContainer();\n\t\tcontainer.appendChild(formatReturnValueAsHtml(ret));\n\t\tcgi.write(container.parentDocument.toString(), true);\n\t}\n\n\t/++\n\n\t\tHistory:\n\t\t\tAdded January 23, 2023 (dub v11.0)\n\t+/\n\tvoid presentExceptionalReturn(Meta)(Cgi cgi, Throwable t, Meta meta, string format) {\n\t\tswitch(format) {\n\t\t\tcase \"html\":\n\t\t\t\tpresentExceptionAsHtml(cgi, t, meta);\n\t\t\tbreak;\n\t\t\tdefault:\n\t\t}\n\t}\n\n\n\t/++\n\t\tIf you override this, you will need to cast the exception type `t` dynamically,\n\t\tbut can then use the template arguments here to refer back to the function.\n\n\t\t`func` is an alias to the method itself, and `dg` is a callable delegate to the same\n\t\tmethod on the live object. You could, in theory, change arguments and retry, but I\n\t\tprovide that information mostly with the expectation that you will use them to make\n\t\tuseful forms or richer error messages for the user.\n\n\t\tHistory:\n\t\t\tBREAKING CHANGE on January 23, 2023 (v11.0 ): it previously took an `alias func` and `T dg` to call the function again.\n\t\t\tI removed this in favor of a `Meta` param.\n\n\t\t\tBefore: `void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg)`\n\n\t\t\tAfter: `void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta)`\n\n\t\t\tIf you used the func for something, move that something into your `methodMeta` template.\n\n\t\t\tWhat is the benefit of this change? Somewhat smaller executables and faster builds thanks to more reused functions, together with\n\t\t\tenabling an easier implementation of [presentExceptionalReturn].\n\t+/\n\tvoid presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta) {\n\t\tForm af;\n\t\t/+\n\t\tforeach(attr; __traits(getAttributes, func)) {\n\t\t\tstatic if(__traits(isSame, attr, AutomaticForm)) {\n\t\t\t\taf = createAutomaticFormForFunction!(func)(dg);\n\t\t\t}\n\t\t}\n\t\t+/\n\t\tpresentExceptionAsHtmlImpl(cgi, t, af);\n\t}\n\n\tvoid presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) {\n\t\tif(auto e = cast(ResourceNotFoundException) t) {\n\t\t\tauto container = this.htmlContainer();\n\n\t\t\tcontainer.addChild(\"p\", e.msg);\n\n\t\t\tif(!cgi.outputtedResponseData)\n\t\t\t\tcgi.setResponseStatus(\"404 Not Found\");\n\t\t\tcgi.write(container.parentDocument.toString(), true);\n\t\t} else if(auto mae = cast(MissingArgumentException) t) {\n\t\t\tif(automaticForm is null)\n\t\t\t\tgoto generic;\n\t\t\tauto container = this.htmlContainer();\n\t\t\tif(cgi.requestMethod == Cgi.RequestMethod.POST)\n\t\t\t\tcontainer.appendChild(Element.make(\"p\", \"Argument `\" ~ mae.argumentName ~ \"` of type `\" ~ mae.argumentType ~ \"` is missing\"));\n\t\t\tcontainer.appendChild(automaticForm);\n\n\t\t\tcgi.write(container.parentDocument.toString(), true);\n\t\t} else {\n\t\t\tgeneric:\n\t\t\tauto container = this.htmlContainer();\n\n\t\t\t// import std.stdio; writeln(t.toString());\n\n\t\t\tcontainer.appendChild(exceptionToElement(t));\n\n\t\t\tcontainer.addChild(\"h4\", \"GET\");\n\t\t\tforeach(k, v; cgi.get) {\n\t\t\t\tauto deets = container.addChild(\"details\");\n\t\t\t\tdeets.addChild(\"summary\", k);\n\t\t\t\tdeets.addChild(\"div\", v);\n\t\t\t}\n\n\t\t\tcontainer.addChild(\"h4\", \"POST\");\n\t\t\tforeach(k, v; cgi.post) {\n\t\t\t\tauto deets = container.addChild(\"details\");\n\t\t\t\tdeets.addChild(\"summary\", k);\n\t\t\t\tdeets.addChild(\"div\", v);\n\t\t\t}\n\n\n\t\t\tif(!cgi.outputtedResponseData)\n\t\t\t\tcgi.setResponseStatus(\"500 Internal Server Error\");\n\t\t\tcgi.write(container.parentDocument.toString(), true);\n\t\t}\n\t}\n\n\tElement exceptionToElement(Throwable t) {\n\t\tauto div = Element.make(\"div\");\n\t\tdiv.addClass(\"exception-display\");\n\n\t\tdiv.addChild(\"p\", t.msg);\n\t\tdiv.addChild(\"p\", \"Inner code origin: \" ~ typeid(t).name ~ \"@\" ~ t.file ~ \":\" ~ to!string(t.line));\n\n\t\tauto pre = div.addChild(\"pre\");\n\t\tstring s;\n\t\ts = t.toString();\n\t\tElement currentBox;\n\t\tbool on = false;\n\t\tforeach(line; s.splitLines) {\n\t\t\tif(!on && line.startsWith(\"-----\"))\n\t\t\t\ton = true;\n\t\t\tif(!on) continue;\n\t\t\tif(line.indexOf(\"arsd/\") != -1) {\n\t\t\t\tif(currentBox is null) {\n\t\t\t\t\tcurrentBox = pre.addChild(\"details\");\n\t\t\t\t\tcurrentBox.addChild(\"summary\", \"Framework code\");\n\t\t\t\t}\n\t\t\t\tcurrentBox.addChild(\"span\", line ~ \"\\n\");\n\t\t\t} else {\n\t\t\t\tpre.addChild(\"span\", line ~ \"\\n\");\n\t\t\t\tcurrentBox = null;\n\t\t\t}\n\t\t}\n\n\t\treturn div;\n\t}\n\n\t/++\n\t\tReturns an element for a particular type\n\t+/\n\tElement elementFor(T)(string displayName, string name, Element function() udaSuggestion) {\n\t\timport std.traits;\n\n\t\tauto div = Element.make(\"div\");\n\t\tdiv.addClass(\"form-field\");\n\n\t\tstatic if(is(T == Cgi.UploadedFile)) {\n\t\t\tElement lbl;\n\t\t\tif(displayName !is null) {\n\t\t\t\tlbl = div.addChild(\"label\");\n\t\t\t\tlbl.addChild(\"span\", displayName, \"label-text\");\n\t\t\t\tlbl.appendText(\" \");\n\t\t\t} else {\n\t\t\t\tlbl = div;\n\t\t\t}\n\t\t\tauto i = lbl.addChild(\"input\", name);\n\t\t\ti.attrs.name = name;\n\t\t\ti.attrs.type = \"file\";\n\t\t} else static if(is(T == enum)) {\n\t\t\tElement lbl;\n\t\t\tif(displayName !is null) {\n\t\t\t\tlbl = div.addChild(\"label\");\n\t\t\t\tlbl.addChild(\"span\", displayName, \"label-text\");\n\t\t\t\tlbl.appendText(\" \");\n\t\t\t} else {\n\t\t\t\tlbl = div;\n\t\t\t}\n\t\t\tauto i = lbl.addChild(\"select\", name);\n\t\t\ti.attrs.name = name;\n\n\t\t\tforeach(memberName; __traits(allMembers, T))\n\t\t\t\ti.addChild(\"option\", memberName);\n\n\t\t} else static if(is(T == struct)) {\n\t\t\tif(displayName !is null)\n\t\t\t\tdiv.addChild(\"span\", displayName, \"label-text\");\n\t\t\tauto fieldset = div.addChild(\"fieldset\");\n\t\t\tfieldset.addChild(\"legend\", beautify(T.stringof)); // FIXME\n\t\t\tfieldset.addChild(\"input\", name);\n\t\t\tforeach(idx, memberName; __traits(allMembers, T))\n\t\t\tstatic if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {\n\t\t\t\tfieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ \".\" ~ memberName, null /* FIXME: pull off the UDA */));\n\t\t\t}\n\t\t} else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) {\n\t\t\tElement lbl;\n\t\t\tif(displayName !is null) {\n\t\t\t\tlbl = div.addChild(\"label\");\n\t\t\t\tlbl.addChild(\"span\", displayName, \"label-text\");\n\t\t\t\tlbl.appendText(\" \");\n\t\t\t} else {\n\t\t\t\tlbl = div;\n\t\t\t}\n\t\t\tElement i;\n\t\t\tif(udaSuggestion) {\n\t\t\t\ti = udaSuggestion();\n\t\t\t\tlbl.appendChild(i);\n\t\t\t} else {\n\t\t\t\ti = lbl.addChild(\"input\", name);\n\t\t\t}\n\t\t\ti.attrs.name = name;\n\t\t\tstatic if(isSomeString!T)\n\t\t\t\ti.attrs.type = \"text\";\n\t\t\telse\n\t\t\t\ti.attrs.type = \"number\";\n\t\t\tif(i.tagName == \"textarea\")\n\t\t\t\ti.textContent = to!string(T.init);\n\t\t\telse\n\t\t\t\ti.attrs.value = to!string(T.init);\n\t\t} else static if(is(T == bool)) {\n\t\t\tElement lbl;\n\t\t\tif(displayName !is null) {\n\t\t\t\tlbl = div.addChild(\"label\");\n\t\t\t\tlbl.addChild(\"span\", displayName, \"label-text\");\n\t\t\t\tlbl.appendText(\" \");\n\t\t\t} else {\n\t\t\t\tlbl = div;\n\t\t\t}\n\t\t\tauto i = lbl.addChild(\"input\", name);\n\t\t\ti.attrs.type = \"checkbox\";\n\t\t\ti.attrs.value = \"true\";\n\t\t\ti.attrs.name = name;\n\t\t} else static if(is(T == K[], K)) {\n\t\t\tauto templ = div.addChild(\"template\");\n\t\t\ttempl.appendChild(elementFor!(K)(null, name, null /* uda??*/));\n\t\t\tif(displayName !is null)\n\t\t\t\tdiv.addChild(\"span\", displayName, \"label-text\");\n\t\t\tauto btn = div.addChild(\"button\");\n\t\t\tbtn.addClass(\"add-array-button\");\n\t\t\tbtn.attrs.type = \"button\";\n\t\t\tbtn.innerText = \"Add\";\n\t\t\tbtn.attrs.onclick = q{\n\t\t\t\tvar a = document.importNode(this.parentNode.firstChild.content, true);\n\t\t\t\tthis.parentNode.insertBefore(a, this);\n\t\t\t};\n\t\t} else static if(is(T == V[K], K, V)) {\n\t\t\tdiv.innerText = \"assoc array not implemented for automatic form at this time\";\n\t\t} else {\n\t\t\tstatic assert(0, \"unsupported type for cgi call \" ~ T.stringof);\n\t\t}\n\n\n\t\treturn div;\n\t}\n\n\t/// creates a form for gathering the function's arguments\n\tForm createAutomaticFormForFunction(alias method, T)(T dg) {\n\n\t\tauto form = cast(Form) Element.make(\"form\");\n\n\t\tform.method = \"POST\"; // FIXME\n\n\t\tform.addClass(\"automatic-form\");\n\n\t\tstring formDisplayName = beautify(__traits(identifier, method));\n\t\tforeach(attr; __traits(getAttributes, method))\n\t\t\tstatic if(is(typeof(attr) == DisplayName))\n\t\t\t\tformDisplayName = attr.name;\n\t\tform.addChild(\"h3\", formDisplayName);\n\n\t\timport std.traits;\n\n\t\t//Parameters!method params;\n\t\t//alias idents = ParameterIdentifierTuple!method;\n\t\t//alias defaults = ParameterDefaults!method;\n\n\t\tstatic if(is(typeof(method) P == __parameters))\n\t\tforeach(idx, _; P) {{\n\n\t\t\talias param = P[idx .. idx + 1];\n\n\t\t\tstatic if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) {\n\t\t\t\tstring displayName = beautify(__traits(identifier, param));\n\t\t\t\tElement function() element;\n\t\t\t\tforeach(attr; __traits(getAttributes, param)) {\n\t\t\t\t\tstatic if(is(typeof(attr) == DisplayName))\n\t\t\t\t\t\tdisplayName = attr.name;\n\t\t\t\t\telse static if(is(typeof(attr) : typeof(element))) {\n\t\t\t\t\t\telement = attr;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tauto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param), element));\n\t\t\t\tif(i.querySelector(\"input[type=file]\") !is null)\n\t\t\t\t\tform.setAttribute(\"enctype\", \"multipart/form-data\");\n\t\t\t}\n\t\t}}\n\n\t\tform.addChild(\"div\", Html(`<input type=\"submit\" value=\"Submit\" />`), \"submit-button-holder\");\n\n\t\treturn form;\n\t}\n\n\t/// creates a form for gathering object members (for the REST object thing right now)\n\tForm createAutomaticFormForObject(T)(T obj) {\n\t\tauto form = cast(Form) Element.make(\"form\");\n\n\t\tform.addClass(\"automatic-form\");\n\n\t\tform.addChild(\"h3\", beautify(__traits(identifier, T)));\n\n\t\timport std.traits;\n\n\t\t//Parameters!method params;\n\t\t//alias idents = ParameterIdentifierTuple!method;\n\t\t//alias defaults = ParameterDefaults!method;\n\n\t\tforeach(idx, memberName; __traits(derivedMembers, T)) {{\n\t\tstatic if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {\n\t\t\tstring displayName = beautify(memberName);\n\t\t\tElement function() element;\n\t\t\tforeach(attr; __traits(getAttributes,  __traits(getMember, T, memberName)))\n\t\t\t\tstatic if(is(typeof(attr) == DisplayName))\n\t\t\t\t\tdisplayName = attr.name;\n\t\t\t\telse static if(is(typeof(attr) : typeof(element)))\n\t\t\t\t\telement = attr;\n\t\t\tform.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName, element));\n\n\t\t\tform.setValue(memberName, to!string(__traits(getMember, obj, memberName)));\n\t\t}}}\n\n\t\tform.addChild(\"div\", Html(`<input type=\"submit\" value=\"Submit\" />`), \"submit-button-holder\");\n\n\t\treturn form;\n\t}\n\n\t///\n\tElement formatReturnValueAsHtml(T)(T t) {\n\t\timport std.traits;\n\n\t\tstatic if(is(T == typeof(null))) {\n\t\t\treturn Element.make(\"span\");\n\t\t} else static if(is(T : Element)) {\n\t\t\treturn t;\n\t\t} else static if(is(T == MultipleResponses!Types, Types...)) {\n\t\t\tforeach(index, type; Types) {\n\t\t\t\tif(t.contains == index)\n\t\t\t\t\treturn formatReturnValueAsHtml(t.payload[index]);\n\t\t\t}\n\t\t\tassert(0);\n\t\t} else static if(is(T == Paginated!E, E)) {\n\t\t\tauto e = Element.make(\"div\").addClass(\"paginated-result\");\n\t\t\te.appendChild(formatReturnValueAsHtml(t.items));\n\t\t\tif(t.nextPageUrl.length)\n\t\t\t\te.appendChild(Element.make(\"a\", \"Next Page\", t.nextPageUrl));\n\t\t\treturn e;\n\t\t} else static if(isIntegral!T || isSomeString!T || isFloatingPoint!T) {\n\t\t\treturn Element.make(\"span\", to!string(t), \"automatic-data-display\");\n\t\t} else static if(is(T == V[K], K, V)) {\n\t\t\tauto dl = Element.make(\"dl\");\n\t\t\tdl.addClass(\"automatic-data-display associative-array\");\n\t\t\tforeach(k, v; t) {\n\t\t\t\tdl.addChild(\"dt\", to!string(k));\n\t\t\t\tdl.addChild(\"dd\", formatReturnValueAsHtml(v));\n\t\t\t}\n\t\t\treturn dl;\n\t\t} else static if(is(T == struct)) {\n\t\t\tauto dl = Element.make(\"dl\");\n\t\t\tdl.addClass(\"automatic-data-display struct\");\n\n\t\t\tforeach(idx, memberName; __traits(allMembers, T))\n\t\t\tstatic if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {\n\t\t\t\tdl.addChild(\"dt\", beautify(memberName));\n\t\t\t\tdl.addChild(\"dd\", formatReturnValueAsHtml(__traits(getMember, t, memberName)));\n\t\t\t}\n\n\t\t\treturn dl;\n\t\t} else static if(is(T == bool)) {\n\t\t\treturn Element.make(\"span\", t ? \"true\" : \"false\", \"automatic-data-display\");\n\t\t} else static if(is(T == E[], E)) {\n\t\t\tstatic if(is(E : RestObject!Proxy, Proxy)) {\n\t\t\t\t// treat RestObject similar to struct\n\t\t\t\tauto table = cast(Table) Element.make(\"table\");\n\t\t\t\ttable.addClass(\"automatic-data-display\");\n\t\t\t\tstring[] names;\n\t\t\t\tforeach(idx, memberName; __traits(derivedMembers, E))\n\t\t\t\tstatic if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {\n\t\t\t\t\tnames ~= beautify(memberName);\n\t\t\t\t}\n\t\t\t\ttable.appendHeaderRow(names);\n\n\t\t\t\tforeach(l; t) {\n\t\t\t\t\tauto tr = table.appendRow();\n\t\t\t\t\tforeach(idx, memberName; __traits(derivedMembers, E))\n\t\t\t\t\tstatic if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {\n\t\t\t\t\t\tstatic if(memberName == \"id\") {\n\t\t\t\t\t\t\tstring val = to!string(__traits(getMember, l, memberName));\n\t\t\t\t\t\t\ttr.addChild(\"td\", Element.make(\"a\", val, E.stringof.toLower ~ \"s/\" ~ val)); // FIXME\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttr.addChild(\"td\", formatReturnValueAsHtml(__traits(getMember, l, memberName)));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn table;\n\t\t\t} else static if(is(E == struct)) {\n\t\t\t\t// an array of structs is kinda special in that I like\n\t\t\t\t// having those formatted as tables.\n\t\t\t\tauto table = cast(Table) Element.make(\"table\");\n\t\t\t\ttable.addClass(\"automatic-data-display\");\n\t\t\t\tstring[] names;\n\t\t\t\tforeach(idx, memberName; __traits(allMembers, E))\n\t\t\t\tstatic if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {\n\t\t\t\t\tnames ~= beautify(memberName);\n\t\t\t\t}\n\t\t\t\ttable.appendHeaderRow(names);\n\n\t\t\t\tforeach(l; t) {\n\t\t\t\t\tauto tr = table.appendRow();\n\t\t\t\t\tforeach(idx, memberName; __traits(allMembers, E))\n\t\t\t\t\tstatic if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {\n\t\t\t\t\t\ttr.addChild(\"td\", formatReturnValueAsHtml(__traits(getMember, l, memberName)));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn table;\n\t\t\t} else {\n\t\t\t\t// otherwise, I will just make a list.\n\t\t\t\tauto ol = Element.make(\"ol\");\n\t\t\t\tol.addClass(\"automatic-data-display\");\n\t\t\t\tforeach(e; t)\n\t\t\t\t\tol.addChild(\"li\", formatReturnValueAsHtml(e));\n\t\t\t\treturn ol;\n\t\t\t}\n\t\t} else static if(is(T : Object)) {\n\t\t\tstatic if(is(typeof(t.toHtml()))) // FIXME: maybe i will make this an interface\n\t\t\t\treturn Element.make(\"div\", t.toHtml());\n\t\t\telse\n\t\t\t\treturn Element.make(\"div\", t.toString());\n\t\t} else static assert(0, \"bad return value for cgi call \" ~ T.stringof);\n\n\t\tassert(0);\n\t}\n\n}\n\n/++\n\tThe base class for the [dispatcher] function and object support.\n+/\nclass WebObject {\n\t//protected Cgi cgi;\n\n\tprotected void initialize(Cgi cgi) {\n\t\t//this.cgi = cgi;\n\t}\n}\n\n/++\n\tCan return one of the given types, decided at runtime. The syntax\n\tis to declare all the possible types in the return value, then you\n\tcan `return typeof(return)(...value...)` to construct it.\n\n\tIt has an auto-generated constructor for each value it can hold.\n\n\t---\n\tMultipleResponses!(Redirection, string) getData(int how) {\n\t\tif(how & 1)\n\t\t\treturn typeof(return)(Redirection(\"http://dpldocs.info/\"));\n\t\telse\n\t\t\treturn typeof(return)(\"hi there!\");\n\t}\n\t---\n\n\tIf you have lots of returns, you could, inside the function, `alias r = typeof(return);` to shorten it a little.\n+/\nstruct MultipleResponses(T...) {\n\tprivate size_t contains;\n\tprivate union {\n\t\tprivate T payload;\n\t}\n\n\tstatic foreach(index, type; T)\n\tpublic this(type t) {\n\t\tcontains = index;\n\t\tpayload[index] = t;\n\t}\n\n\t/++\n\t\tThis is primarily for testing. It is your way of getting to the response.\n\n\t\tLet's say you wanted to test that one holding a Redirection and a string actually\n\t\tholds a string, by name of \"test\":\n\n\t\t---\n\t\t\tauto valueToTest = your_test_function();\n\n\t\t\tvalueToTest.visit(\n\t\t\t\t(Redirection r) { assert(0); }, // got a redirection instead of a string, fail the test\n\t\t\t\t(string s) { assert(s == \"test\"); } // right value, go ahead and test it.\n\t\t\t);\n\t\t---\n\n\t\tHistory:\n\t\t\tWas horribly broken until June 16, 2022. Ironically, I wrote it for tests but never actually tested it.\n\t\t\tIt tried to use alias lambdas before, but runtime delegates work much better so I changed it.\n\t+/\n\tvoid visit(Handlers...)(Handlers handlers) {\n\t\ttemplate findHandler(type, int count, HandlersToCheck...) {\n\t\t\tstatic if(HandlersToCheck.length == 0)\n\t\t\t\tenum findHandler = -1;\n\t\t\telse {\n\t\t\t\tstatic if(is(typeof(HandlersToCheck[0].init(type.init))))\n\t\t\t\t\tenum findHandler = count;\n\t\t\t\telse\n\t\t\t\t\tenum findHandler = findHandler!(type, count + 1, HandlersToCheck[1 .. $]);\n\t\t\t}\n\t\t}\n\t\tforeach(index, type; T) {\n\t\t\tenum handlerIndex = findHandler!(type, 0, Handlers);\n\t\t\tstatic if(handlerIndex == -1)\n\t\t\t\tstatic assert(0, \"Type \" ~ type.stringof ~ \" was not handled by visitor\");\n\t\t\telse {\n\t\t\t\tif(index == this.contains)\n\t\t\t\t\thandlers[handlerIndex](this.payload[index]);\n\t\t\t}\n\t\t}\n\t}\n\n\t/+\n\tauto toArsdJsvar()() {\n\t\timport arsd.jsvar;\n\t\treturn var(null);\n\t}\n\t+/\n}\n\n// FIXME: implement this somewhere maybe\nstruct RawResponse {\n\tint code;\n\tstring[] headers;\n\tconst(ubyte)[] responseBody;\n}\n\n/++\n\tYou can return this from [WebObject] subclasses for redirections.\n\n\t(though note the static types means that class must ALWAYS redirect if\n\tyou return this directly. You might want to return [MultipleResponses] if it\n\tcan be conditional)\n+/\nstruct Redirection {\n\tstring to; /// The URL to redirect to.\n\tint code = 303; /// The HTTP code to return.\n}\n\n/++\n\tServes a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher].\n\n\tUsage of this function will add a dependency on [arsd.dom] and [arsd.jsvar] unless you have overridden\n\tthe presenter in the dispatcher.\n\n\tFIXME: explain this better\n\n\tYou can overload functions to a limited extent: you can provide a zero-arg and non-zero-arg function,\n\tand non-zero-arg functions can filter via UDAs for various http methods. Do not attempt other overloads,\n\tthe runtime result of that is undefined.\n\n\tA method is assumed to allow any http method unless it lists some in UDAs, in which case it is limited to only those.\n\t(this might change, like maybe i will use pure as an indicator GET is ok. idk.)\n\n\t$(WARNING\n\t\t---\n\t\t// legal in D, undefined runtime behavior with cgi.d, it may call either method\n\t\t// even if you put different URL udas on it, the current code ignores them.\n\t\tvoid foo(int a) {}\n\t\tvoid foo(string a) {}\n\t\t---\n\t)\n\n\tSee_Also: [serveRestObject], [serveStaticFile]\n+/\nauto serveApi(T)(string urlPrefix) {\n\tassert(urlPrefix[$ - 1] == '/');\n\treturn serveApiInternal!T(urlPrefix);\n}\n\nprivate string nextPieceFromSlash(ref string remainingUrl) {\n\tif(remainingUrl.length == 0)\n\t\treturn remainingUrl;\n\tint slash = 0;\n\twhile(slash < remainingUrl.length && remainingUrl[slash] != '/') // && remainingUrl[slash] != '.')\n\t\tslash++;\n\n\t// I am specifically passing `null` to differentiate it vs empty string\n\t// so in your ctor, `items` means new T(null) and `items/` means new T(\"\")\n\tauto ident = remainingUrl.length == 0 ? null : remainingUrl[0 .. slash];\n\t// so if it is the last item, the dot can be used to load an alternative view\n\t// otherwise tho the dot is considered part of the identifier\n\t// FIXME\n\n\t// again notice \"\" vs null here!\n\tif(slash == remainingUrl.length)\n\t\tremainingUrl = null;\n\telse\n\t\tremainingUrl = remainingUrl[slash + 1 .. $];\n\n\treturn ident;\n}\n\n/++\n\tUDA 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.\n+/\nenum AddTrailingSlash;\n/// ditto\nenum RemoveTrailingSlash;\n\nprivate auto serveApiInternal(T)(string urlPrefix) {\n\n\timport arsd.dom;\n\timport arsd.jsvar;\n\n\tstatic bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) {\n\t\tstring remainingUrl = cgi.pathInfo[urlPrefix.length .. $];\n\n\t\ttry {\n\t\t\t// see duplicated code below by searching subresource_ctor\n\t\t\t// also see mustNotBeSetFromWebParams\n\n\t\t\tstatic if(is(typeof(T.__ctor) P == __parameters)) {\n\t\t\t\tP params;\n\n\t\t\t\tforeach(pidx, param; P) {\n\t\t\t\t\tstatic if(is(param : Cgi)) {\n\t\t\t\t\t\tstatic assert(!is(param == immutable));\n\t\t\t\t\t\tcast() params[pidx] = cgi;\n\t\t\t\t\t} else static if(is(param == Session!D, D)) {\n\t\t\t\t\t\tstatic assert(!is(param == immutable));\n\t\t\t\t\t\tcast() params[pidx] = cgi.getSessionObject!D();\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstatic if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) {\n\t\t\t\t\t\t\tforeach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) {\n\t\t\t\t\t\t\t\tstatic if(is(uda == ifCalledFromWeb!func, alias func)) {\n\t\t\t\t\t\t\t\t\tstatic if(is(typeof(func(cgi))))\n\t\t\t\t\t\t\t\t\t\tparams[pidx] = func(cgi);\n\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\tparams[pidx] = func();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tstatic if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) {\n\t\t\t\t\t\t\t\tparams[pidx] = param.getAutomaticallyForCgi(cgi);\n\t\t\t\t\t\t\t} else static if(is(param == string)) {\n\t\t\t\t\t\t\t\tauto ident = nextPieceFromSlash(remainingUrl);\n\t\t\t\t\t\t\t\tparams[pidx] = ident;\n\t\t\t\t\t\t\t} else static assert(0, \"illegal type for subresource \" ~ param.stringof);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tauto obj = new T(params);\n\t\t\t} else {\n\t\t\t\tauto obj = new T();\n\t\t\t}\n\n\t\t\treturn internalHandlerWithObject(obj, remainingUrl, cgi, presenter);\n\t\t} catch(Throwable t) {\n\t\t\tswitch(cgi.request(\"format\", \"html\")) {\n\t\t\t\tcase \"html\":\n\t\t\t\t\tstatic void dummy() {}\n\t\t\t\t\tpresenter.presentExceptionAsHtml(cgi, t, null);\n\t\t\t\treturn true;\n\t\t\t\tcase \"json\":\n\t\t\t\t\tvar envelope = var.emptyObject;\n\t\t\t\t\tenvelope.success = false;\n\t\t\t\t\tenvelope.result = null;\n\t\t\t\t\tenvelope.error = t.toString();\n\t\t\t\t\tcgi.setResponseContentType(\"application/json\");\n\t\t\t\t\tcgi.write(envelope.toJson(), true);\n\t\t\t\treturn true;\n\t\t\t\tdefault:\n\t\t\t\t\tthrow t;\n\t\t\t\t// return true;\n\t\t\t}\n\t\t\t// return true;\n\t\t}\n\n\t\tassert(0);\n\t}\n\n\tstatic bool internalHandlerWithObject(T, Presenter)(T obj, string remainingUrl, Cgi cgi, Presenter presenter) {\n\n\t\tobj.initialize(cgi);\n\n\t\t/+\n\t\t\tOverload rules:\n\t\t\t\tAny unique combination of HTTP verb and url path can be dispatched to function overloads\n\t\t\t\tstatically.\n\n\t\t\t\tMoreover, some args vs no args can be overloaded dynamically.\n\t\t+/\n\n\t\tauto methodNameFromUrl = nextPieceFromSlash(remainingUrl);\n\t\t/+\n\t\tauto orig = remainingUrl;\n\t\tassert(0,\n\t\t\t(orig is null ? \"__null\" : orig)\n\t\t\t~ \" .. \" ~\n\t\t\t(methodNameFromUrl is null ? \"__null\" : methodNameFromUrl));\n\t\t+/\n\n\t\tif(methodNameFromUrl is null)\n\t\t\tmethodNameFromUrl = \"__null\";\n\n\t\tstring hack = to!string(cgi.requestMethod) ~ \" \" ~ methodNameFromUrl;\n\n\t\tif(remainingUrl.length)\n\t\t\thack ~= \"/\";\n\n\t\tswitch(hack) {\n\t\t\tforeach(methodName; __traits(derivedMembers, T))\n\t\t\tstatic if(methodName != \"__ctor\")\n\t\t\tforeach(idx, overload; __traits(getOverloads, T, methodName)) {\n\t\t\tstatic if(is(typeof(overload) P == __parameters))\n\t\t\tstatic if(is(typeof(overload) R == return))\n\t\t\tstatic if(__traits(getProtection, overload) == \"public\" || __traits(getProtection, overload) == \"export\")\n\t\t\t{\n\t\t\tstatic foreach(urlNameForMethod; urlNamesForMethod!(overload, urlify(methodName)))\n\t\t\tcase urlNameForMethod:\n\n\t\t\t\tstatic if(is(R : WebObject)) {\n\t\t\t\t\t// if it returns a WebObject, it is considered a subresource. That means the url is dispatched like the ctor above.\n\n\t\t\t\t\t// the only argument it is allowed to take, outside of cgi, session, and set up thingies, is a single string\n\n\t\t\t\t\t// subresource_ctor\n\t\t\t\t\t// also see mustNotBeSetFromWebParams\n\n\t\t\t\t\tP params;\n\n\t\t\t\t\tstring ident;\n\n\t\t\t\t\tforeach(pidx, param; P) {\n\t\t\t\t\t\tstatic if(is(param : Cgi)) {\n\t\t\t\t\t\t\tstatic assert(!is(param == immutable));\n\t\t\t\t\t\t\tcast() params[pidx] = cgi;\n\t\t\t\t\t\t} else static if(is(param == typeof(presenter))) {\n\t\t\t\t\t\t\tcast() param[pidx] = presenter;\n\t\t\t\t\t\t} else static if(is(param == Session!D, D)) {\n\t\t\t\t\t\t\tstatic assert(!is(param == immutable));\n\t\t\t\t\t\t\tcast() params[pidx] = cgi.getSessionObject!D();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tstatic if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) {\n\t\t\t\t\t\t\t\tforeach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) {\n\t\t\t\t\t\t\t\t\tstatic if(is(uda == ifCalledFromWeb!func, alias func)) {\n\t\t\t\t\t\t\t\t\t\tstatic if(is(typeof(func(cgi))))\n\t\t\t\t\t\t\t\t\t\t\tparams[pidx] = func(cgi);\n\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\tparams[pidx] = func();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\tstatic if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) {\n\t\t\t\t\t\t\t\t\tparams[pidx] = param.getAutomaticallyForCgi(cgi);\n\t\t\t\t\t\t\t\t} else static if(is(param == string)) {\n\t\t\t\t\t\t\t\t\tident = nextPieceFromSlash(remainingUrl);\n\t\t\t\t\t\t\t\t\tif(ident is null) {\n\t\t\t\t\t\t\t\t\t\t// trailing slash mandated on subresources\n\t\t\t\t\t\t\t\t\t\tcgi.setResponseLocation(cgi.pathInfo ~ \"/\");\n\t\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tparams[pidx] = ident;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else static assert(0, \"illegal type for subresource \" ~ param.stringof);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tauto nobj = (__traits(getOverloads, obj, methodName)[idx])(ident);\n\t\t\t\t\treturn internalHandlerWithObject!(typeof(nobj), Presenter)(nobj, remainingUrl, cgi, presenter);\n\t\t\t\t} else {\n\t\t\t\t\t// 404 it if any url left - not a subresource means we don't get to play with that!\n\t\t\t\t\tif(remainingUrl.length)\n\t\t\t\t\t\treturn false;\n\n\t\t\t\t\tbool automaticForm;\n\n\t\t\t\t\tforeach(attr; __traits(getAttributes, overload))\n\t\t\t\t\t\tstatic if(is(attr == AddTrailingSlash)) {\n\t\t\t\t\t\t\tif(remainingUrl is null) {\n\t\t\t\t\t\t\t\tcgi.setResponseLocation(cgi.pathInfo ~ \"/\");\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else static if(is(attr == RemoveTrailingSlash)) {\n\t\t\t\t\t\t\tif(remainingUrl !is null) {\n\t\t\t\t\t\t\t\tcgi.setResponseLocation(cgi.pathInfo[0 .. lastIndexOf(cgi.pathInfo, \"/\")]);\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else static if(__traits(isSame, AutomaticForm, attr)) {\n\t\t\t\t\t\t\tautomaticForm = true;\n\t\t\t\t\t\t}\n\n\t\t\t\t/+\n\t\t\t\tint zeroArgOverload = -1;\n\t\t\t\tint overloadCount = cast(int) __traits(getOverloads, T, methodName).length;\n\t\t\t\tbool calledWithZeroArgs = true;\n\t\t\t\tforeach(k, v; cgi.get)\n\t\t\t\t\tif(k != \"format\") {\n\t\t\t\t\t\tcalledWithZeroArgs = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\tforeach(k, v; cgi.post)\n\t\t\t\t\tif(k != \"format\") {\n\t\t\t\t\t\tcalledWithZeroArgs = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t// first, we need to go through and see if there is an empty one, since that\n\t\t\t\t// changes inside. But otherwise, all the stuff I care about can be done via\n\t\t\t\t// simple looping (other improper overloads might be flagged for runtime semantic check)\n\t\t\t\t//\n\t\t\t\t// an argument of type Cgi is ignored for these purposes\n\t\t\t\tstatic foreach(idx, overload; __traits(getOverloads, T, methodName)) {{\n\t\t\t\t\tstatic if(is(typeof(overload) P == __parameters))\n\t\t\t\t\t\tstatic if(P.length == 0)\n\t\t\t\t\t\t\tzeroArgOverload = cast(int) idx;\n\t\t\t\t\t\telse static if(P.length == 1 && is(P[0] : Cgi))\n\t\t\t\t\t\t\tzeroArgOverload = cast(int) idx;\n\t\t\t\t}}\n\t\t\t\t// FIXME: static assert if there are multiple non-zero-arg overloads usable with a single http method.\n\t\t\t\tbool overloadHasBeenCalled = false;\n\t\t\t\tstatic foreach(idx, overload; __traits(getOverloads, T, methodName)) {{\n\t\t\t\t\tbool callFunction = true;\n\t\t\t\t\t// there is a zero arg overload and this is NOT it, and we have zero args - don't call this\n\t\t\t\t\tif(overloadCount > 1 && zeroArgOverload != -1 && idx != zeroArgOverload && calledWithZeroArgs)\n\t\t\t\t\t\tcallFunction = false;\n\t\t\t\t\t// if this is the zero-arg overload, obviously it cannot be called if we got any args.\n\t\t\t\t\tif(overloadCount > 1 && idx == zeroArgOverload && !calledWithZeroArgs)\n\t\t\t\t\t\tcallFunction = false;\n\n\t\t\t\t\t// FIXME: so if you just add ?foo it will give the error below even when. this might not be a great idea.\n\n\t\t\t\t\tbool hadAnyMethodRestrictions = false;\n\t\t\t\t\tbool foundAcceptableMethod = false;\n\t\t\t\t\tforeach(attr; __traits(getAttributes, overload)) {\n\t\t\t\t\t\tstatic if(is(typeof(attr) == Cgi.RequestMethod)) {\n\t\t\t\t\t\t\thadAnyMethodRestrictions = true;\n\t\t\t\t\t\t\tif(attr == cgi.requestMethod)\n\t\t\t\t\t\t\t\tfoundAcceptableMethod = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif(hadAnyMethodRestrictions && !foundAcceptableMethod)\n\t\t\t\t\t\tcallFunction = false;\n\n\t\t\t\t\t/+\n\t\t\t\t\t\tThe overloads we really want to allow are the sane ones\n\t\t\t\t\t\tfrom the web perspective. Which is likely on HTTP verbs,\n\t\t\t\t\t\tfor the most part, but might also be potentially based on\n\t\t\t\t\t\tsome args vs zero args, or on argument names. Can't really\n\t\t\t\t\t\tdo argument types very reliable through the web though; those\n\t\t\t\t\t\tshould probably be different URLs.\n\n\t\t\t\t\t\tEven names I feel is better done inside the function, so I'm not\n\t\t\t\t\t\tgoing to support that here. But the HTTP verbs and zero vs some\n\t\t\t\t\t\targs makes sense - it lets you define custom forms pretty easily.\n\n\t\t\t\t\t\tMoreover, I'm of the opinion that empty overload really only makes\n\t\t\t\t\t\tsense on GET for this case. On a POST, it is just a missing argument\n\t\t\t\t\t\texception and that should be handled by the presenter. But meh, I'll\n\t\t\t\t\t\tlet the user define that, D only allows one empty arg thing anyway\n\t\t\t\t\t\tso the method UDAs are irrelevant.\n\t\t\t\t\t+/\n\t\t\t\t\tif(callFunction)\n\t\t\t\t+/\n\n\t\t\t\t\tauto format = cgi.request(\"format\", defaultFormat!overload());\n\t\t\t\t\tauto wantsFormFormat = format.startsWith(\"form-\");\n\n\t\t\t\t\tif(wantsFormFormat || (automaticForm && cgi.requestMethod == Cgi.RequestMethod.GET)) {\n\t\t\t\t\t\t// Should I still show the form on a json thing? idk...\n\t\t\t\t\t\tauto ret = presenter.createAutomaticFormForFunction!((__traits(getOverloads, obj, methodName)[idx]))(&(__traits(getOverloads, obj, methodName)[idx]));\n\t\t\t\t\t\tpresenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), wantsFormFormat ? format[\"form_\".length .. $] : \"html\");\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// a void return (or typeof(null) lol) means you, the user, is doing it yourself. Gives full control.\n\t\t\t\t\t\tauto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi);\n\t\t\t\t\t\tpresenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format);\n\t\t\t\t\t} catch(Throwable t) {\n\t\t\t\t\t\t// presenter.presentExceptionAsHtml!(__traits(getOverloads, obj, methodName)[idx])(cgi, t, &(__traits(getOverloads, obj, methodName)[idx]));\n\t\t\t\t\t\tpresenter.presentExceptionalReturn(cgi, t, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format);\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t//}}\n\n\t\t\t\t//cgi.header(\"Accept: POST\"); // FIXME list the real thing\n\t\t\t\t//cgi.setResponseStatus(\"405 Method Not Allowed\"); // again, not exactly, but sort of. no overload matched our args, almost certainly due to http verb filtering.\n\t\t\t\t//return true;\n\t\t\t\t}\n\t\t\t}\n\t\t\t}\n\t\t\tcase \"GET script.js\":\n\t\t\t\tcgi.setResponseContentType(\"text/javascript\");\n\t\t\t\tcgi.gzipResponse = true;\n\t\t\t\tcgi.write(presenter.script(), true);\n\t\t\t\treturn true;\n\t\t\tcase \"GET style.css\":\n\t\t\t\tcgi.setResponseContentType(\"text/css\");\n\t\t\t\tcgi.gzipResponse = true;\n\t\t\t\tcgi.write(presenter.style(), true);\n\t\t\t\treturn true;\n\t\t\tdefault:\n\t\t\t\treturn false;\n\t\t}\n\n\t\tassert(0);\n\t}\n\treturn DispatcherDefinition!internalHandler(urlPrefix, false);\n}\n\nstring defaultFormat(alias method)() {\n\tbool nonConstConditionForWorkingAroundASpuriousDmdWarning = true;\n\tforeach(attr; __traits(getAttributes, method)) {\n\t\tstatic if(is(typeof(attr) == DefaultFormat)) {\n\t\t\tif(nonConstConditionForWorkingAroundASpuriousDmdWarning)\n\t\t\t\treturn attr.value;\n\t\t}\n\t}\n\treturn \"html\";\n}\n\nstruct Paginated(T) {\n\tT[] items;\n\tstring nextPageUrl;\n}\n\ntemplate urlNamesForMethod(alias method, string default_) {\n\tstring[] helper() {\n\t\tauto verb = Cgi.RequestMethod.GET;\n\t\tbool foundVerb = false;\n\t\tbool foundNoun = false;\n\n\t\tstring def = default_;\n\n\t\tbool hasAutomaticForm = false;\n\n\t\tforeach(attr; __traits(getAttributes, method)) {\n\t\t\tstatic if(is(typeof(attr) == Cgi.RequestMethod)) {\n\t\t\t\tverb = attr;\n\t\t\t\tif(foundVerb)\n\t\t\t\t\tassert(0, \"Multiple http verbs on one function is not currently supported\");\n\t\t\t\tfoundVerb = true;\n\t\t\t}\n\t\t\tstatic if(is(typeof(attr) == UrlName)) {\n\t\t\t\tif(foundNoun)\n\t\t\t\t\tassert(0, \"Multiple url names on one function is not currently supported\");\n\t\t\t\tfoundNoun = true;\n\t\t\t\tdef = attr.name;\n\t\t\t}\n\t\t\tstatic if(__traits(isSame, attr, AutomaticForm)) {\n\t\t\t\thasAutomaticForm = true;\n\t\t\t}\n\t\t}\n\n\t\tif(def is null)\n\t\t\tdef = \"__null\";\n\n\t\tstring[] ret;\n\n\t\tstatic if(is(typeof(method) R == return)) {\n\t\t\tstatic if(is(R : WebObject)) {\n\t\t\t\tdef ~= \"/\";\n\t\t\t\tforeach(v; __traits(allMembers, Cgi.RequestMethod))\n\t\t\t\t\tret ~= v ~ \" \" ~ def;\n\t\t\t} else {\n\t\t\t\tif(hasAutomaticForm) {\n\t\t\t\t\tret ~= \"GET \" ~ def;\n\t\t\t\t\tret ~= \"POST \" ~ def;\n\t\t\t\t} else {\n\t\t\t\t\tret ~= to!string(verb) ~ \" \" ~ def;\n\t\t\t\t}\n\t\t\t}\n\t\t} else static assert(0);\n\n\t\treturn ret;\n\t}\n\tenum urlNamesForMethod = helper();\n}\n\n\n\tenum AccessCheck {\n\t\tallowed,\n\t\tdenied,\n\t\tnonExistent,\n\t}\n\n\tenum Operation {\n\t\tshow,\n\t\tcreate,\n\t\treplace,\n\t\tremove,\n\t\tupdate\n\t}\n\n\tenum UpdateResult {\n\t\taccessDenied,\n\t\tnoSuchResource,\n\t\tsuccess,\n\t\tfailure,\n\t\tunnecessary\n\t}\n\n\tenum ValidationResult {\n\t\tvalid,\n\t\tinvalid\n\t}\n\n\n/++\n\tThe base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf].\n\n\tWARNING: this is not stable.\n+/\nclass RestObject(CRTP) : WebObject {\n\n\timport arsd.dom;\n\timport arsd.jsvar;\n\n\t/// Prepare the object to be shown.\n\tvoid show() {}\n\t/// ditto\n\tvoid show(string urlId) {\n\t\tload(urlId);\n\t\tshow();\n\t}\n\n\t/// Override this to provide access control to this object.\n\tAccessCheck accessCheck(string urlId, Operation operation) {\n\t\treturn AccessCheck.allowed;\n\t}\n\n\tValidationResult validate() {\n\t\t// FIXME\n\t\treturn ValidationResult.valid;\n\t}\n\n\tstring getUrlSlug() {\n\t\timport std.conv;\n\t\tstatic if(is(typeof(CRTP.id)))\n\t\t\treturn to!string((cast(CRTP) this).id);\n\t\telse\n\t\t\treturn null;\n\t}\n\n\t// The functions with more arguments are the low-level ones,\n\t// they forward to the ones with fewer arguments by default.\n\n\t// POST on a parent collection - this is called from a collection class after the members are updated\n\t/++\n\t\tGiven a populated object, this creates a new entry. Returns the url identifier\n\t\tof the new object.\n\t+/\n\tstring create(scope void delegate() applyChanges) {\n\t\tapplyChanges();\n\t\tsave();\n\t\treturn getUrlSlug();\n\t}\n\n\tvoid replace() {\n\t\tsave();\n\t}\n\tvoid replace(string urlId, scope void delegate() applyChanges) {\n\t\tload(urlId);\n\t\tapplyChanges();\n\t\treplace();\n\t}\n\n\tvoid update(string[] fieldList) {\n\t\tsave();\n\t}\n\tvoid update(string urlId, scope void delegate() applyChanges, string[] fieldList) {\n\t\tload(urlId);\n\t\tapplyChanges();\n\t\tupdate(fieldList);\n\t}\n\n\tvoid remove() {}\n\n\tvoid remove(string urlId) {\n\t\tload(urlId);\n\t\tremove();\n\t}\n\n\tabstract void load(string urlId);\n\tabstract void save();\n\n\tElement toHtml(Presenter)(Presenter presenter) {\n\t\timport arsd.dom;\n\t\timport std.conv;\n\t\tauto obj = cast(CRTP) this;\n\t\tauto div = Element.make(\"div\");\n\t\tdiv.addClass(\"Dclass_\" ~ CRTP.stringof);\n\t\tdiv.dataset.url = getUrlSlug();\n\t\tbool first = true;\n\t\tforeach(idx, memberName; __traits(derivedMembers, CRTP))\n\t\tstatic if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {\n\t\t\tif(!first) div.addChild(\"br\"); else first = false;\n\t\t\tdiv.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName)));\n\t\t}\n\t\treturn div;\n\t}\n\n\tvar toJson() {\n\t\timport arsd.jsvar;\n\t\tvar v = var.emptyObject();\n\t\tauto obj = cast(CRTP) this;\n\t\tforeach(idx, memberName; __traits(derivedMembers, CRTP))\n\t\tstatic if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {\n\t\t\tv[memberName] = __traits(getMember, obj, memberName);\n\t\t}\n\t\treturn v;\n\t}\n\n\t/+\n\tauto structOf(this This) {\n\n\t}\n\t+/\n}\n\n// FIXME XSRF token, prolly can just put in a cookie and then it needs to be copied to header or form hidden value\n// https://use-the-index-luke.com/sql/partial-results/fetch-next-page\n\n/++\n\tBase class for REST collections.\n+/\nclass CollectionOf(Obj) : RestObject!(CollectionOf) {\n\t/// You might subclass this and use the cgi object's query params\n\t/// to implement a search filter, for example.\n\t///\n\t/// FIXME: design a way to auto-generate that form\n\t/// (other than using the WebObject thing above lol\n\t// it'll prolly just be some searchParams UDA or maybe an enum.\n\t//\n\t// pagination too perhaps.\n\t//\n\t// and sorting too\n\tIndexResult index() { return IndexResult.init; }\n\n\tstring[] sortableFields() { return null; }\n\tstring[] searchableFields() { return null; }\n\n\tstruct IndexResult {\n\t\tObj[] results;\n\n\t\tstring[] sortableFields;\n\n\t\tstring previousPageIdentifier;\n\t\tstring nextPageIdentifier;\n\t\tstring firstPageIdentifier;\n\t\tstring lastPageIdentifier;\n\n\t\tint numberOfPages;\n\t}\n\n\toverride string create(scope void delegate() applyChanges) { assert(0); }\n\toverride void load(string urlId) { assert(0); }\n\toverride void save() { assert(0); }\n\toverride void show() {\n\t\tindex();\n\t}\n\toverride void show(string urlId) {\n\t\tshow();\n\t}\n\n\t/// Proxy POST requests (create calls) to the child collection\n\talias PostProxy = Obj;\n}\n\n/++\n\tServes a REST object, similar to a Ruby on Rails resource.\n\n\tYou put data members in your class. cgi.d will automatically make something out of those.\n\n\tIt will call your constructor with the ID from the URL. This may be null.\n\tIt will then populate the data members from the request.\n\tIt will then call a method, if present, telling what happened. You don't need to write these!\n\tIt finally returns a reply.\n\n\tYour methods are passed a list of fields it actually set.\n\n\tThe URL mapping - despite my general skepticism of the wisdom - matches up with what most REST\n\tAPIs I have used seem to follow. (I REALLY want to put trailing slashes on it though. Works better\n\twith relative linking. But meh.)\n\n\tGET /items -> index. all values not set.\n\tGET /items/id -> get. only ID will be set, other params ignored.\n\tPOST /items -> create. values set as given\n\tPUT /items/id -> replace. values set as given\n\t\tor POST /items/id with cgi.post[\"_method\"] (thus urlencoded or multipart content-type) set to \"PUT\" to work around browser/html limitation\n\t\ta GET with cgi.get[\"_method\"] (in the url) set to \"PUT\" will render a form.\n\tPATCH /items/id -> update. values set as given, list of changed fields passed\n\t\tor POST /items/id with cgi.post[\"_method\"] == \"PATCH\"\n\tDELETE /items/id -> destroy. only ID guaranteed to be set\n\t\tor POST /items/id with cgi.post[\"_method\"] == \"DELETE\"\n\n\tFollowing the stupid convention, there will never be a trailing slash here, and if it is there, it will\n\tredirect you away from it.\n\n\tAPI clients should set the `Accept` HTTP header to application/json or the cgi.get[\"_format\"] = \"json\" var.\n\n\tI will also let you change the default, if you must.\n\n\t// One add-on is validation. You can issue a HTTP GET to a resource with _method = VALIDATE to check potential changes.\n\n\tYou can define sub-resources on your object inside the object. These sub-resources are also REST objects\n\tthat follow the same thing. They may be individual resources or collections themselves.\n\n\tYour class is expected to have at least the following methods:\n\n\tFIXME: i kinda wanna add a routes object to the initialize call\n\n\tcreate\n\t\tCreate returns the new address on success, some code on failure.\n\tshow\n\tindex\n\tupdate\n\tremove\n\n\tYou will want to be able to customize the HTTP, HTML, and JSON returns but generally shouldn't have to - the defaults\n\tshould usually work. The returned JSON will include a field \"href\" on all returned objects along with \"id\". Or something like that.\n\n\tUsage of this function will add a dependency on [arsd.dom] and [arsd.jsvar].\n\n\tNOT IMPLEMENTED\n\n\n\tReally, a collection is a resource with a bunch of subresources.\n\n\t\tGET /items\n\t\t\tindex because it is GET on the top resource\n\n\t\tGET /items/foo\n\t\t\titem but different than items?\n\n\t\tclass Items {\n\n\t\t}\n\n\t... but meh, a collection can be automated. not worth making it\n\ta separate thing, let's look at a real example. Users has many\n\titems and a virtual one, /users/current.\n\n\tthe individual users have properties and two sub-resources:\n\tsession, which is just one, and comments, a collection.\n\n\tclass User : RestObject!() { // no parent\n\t\tint id;\n\t\tstring name;\n\n\t\t// the default implementations of the urlId ones is to call load(that_id) then call the arg-less one.\n\t\t// but you can override them to do it differently.\n\n\t\t// any member which is of type RestObject can be linked automatically via href btw.\n\n\t\tvoid show() {}\n\t\tvoid show(string urlId) {} // automated! GET of this specific thing\n\t\tvoid create() {} // POST on a parent collection - this is called from a collection class after the members are updated\n\t\tvoid replace(string urlId) {} // this is the PUT; really, it just updates all fields.\n\t\tvoid update(string urlId, string[] fieldList) {} // PATCH, it updates some fields.\n\t\tvoid remove(string urlId) {} // DELETE\n\n\t\tvoid load(string urlId) {} // the default implementation of show() populates the id, then\n\n\t\tthis() {}\n\n\t\tmixin Subresource!Session;\n\t\tmixin Subresource!Comment;\n\t}\n\n\tclass Session : RestObject!() {\n\t\t// the parent object may not be fully constructed/loaded\n\t\tthis(User parent) {}\n\n\t}\n\n\tclass Comment : CollectionOf!Comment {\n\t\tthis(User parent) {}\n\t}\n\n\tclass Users : CollectionOf!User {\n\t\t// but you don't strictly need ANYTHING on a collection; it will just... collect. Implement the subobjects.\n\t\tvoid index() {} // GET on this specific thing; just like show really, just different name for the different semantics.\n\t\tUser 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\n\t}\n\n+/\nauto serveRestObject(T)(string urlPrefix) {\n\tassert(urlPrefix[0] == '/');\n\tassert(urlPrefix[$ - 1] != '/', \"Do NOT use a trailing slash on REST objects.\");\n\tstatic bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) {\n\t\tstring url = cgi.pathInfo[urlPrefix.length .. $];\n\n\t\tif(url.length && url[$ - 1] == '/') {\n\t\t\t// remove the final slash...\n\t\t\tcgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo[0 .. $ - 1]);\n\t\t\treturn true;\n\t\t}\n\n\t\treturn restObjectServeHandler!T(cgi, presenter, url);\n\t}\n\treturn DispatcherDefinition!internalHandler(urlPrefix, false);\n}\n\n/+\n/// Convenience method for serving a collection. It will be named the same\n/// as type T, just with an s at the end. If you need any further, just\n/// write the class yourself.\nauto serveRestCollectionOf(T)(string urlPrefix) {\n\tassert(urlPrefix[0] == '/');\n\tmixin(`static class `~T.stringof~`s : CollectionOf!(T) {}`);\n\treturn serveRestObject!(mixin(T.stringof ~ \"s\"))(urlPrefix);\n}\n+/\n\nbool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string url) {\n\tstring urlId = null;\n\tif(url.length && url[0] == '/') {\n\t\t// asking for a subobject\n\t\turlId = url[1 .. $];\n\t\tforeach(idx, ch; urlId) {\n\t\t\tif(ch == '/') {\n\t\t\t\turlId = urlId[0 .. idx];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// FIXME handle other subresources\n\n\tstatic if(is(T : CollectionOf!(C), C)) {\n\t\tif(urlId !is null) {\n\t\t\treturn restObjectServeHandler!(C, Presenter)(cgi, presenter, url); // FIXME?  urlId);\n\t\t}\n\t}\n\n\t// FIXME: support precondition failed, if-modified-since, expectation failed, etc.\n\n\tauto obj = new T();\n\tobj.initialize(cgi);\n\t// FIXME: populate reflection info delegates\n\n\n\t// FIXME: I am not happy with this.\n\tswitch(urlId) {\n\t\tcase \"script.js\":\n\t\t\tcgi.setResponseContentType(\"text/javascript\");\n\t\t\tcgi.gzipResponse = true;\n\t\t\tcgi.write(presenter.script(), true);\n\t\t\treturn true;\n\t\tcase \"style.css\":\n\t\t\tcgi.setResponseContentType(\"text/css\");\n\t\t\tcgi.gzipResponse = true;\n\t\t\tcgi.write(presenter.style(), true);\n\t\t\treturn true;\n\t\tdefault:\n\t\t\t// intentionally blank\n\t}\n\n\n\n\n\tstatic void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) {\n\t\tforeach(idx, memberName; __traits(derivedMembers, Obj))\n\t\tstatic if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {\n\t\t\t__traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName));\n\t\t}\n\t}\n\tvoid applyChanges() {\n\t\tapplyChangesTemplate(cgi, obj);\n\t}\n\n\tstring[] modifiedList;\n\n\tvoid writeObject(bool addFormLinks) {\n\t\tif(cgi.request(\"format\") == \"json\") {\n\t\t\tcgi.setResponseContentType(\"application/json\");\n\t\t\tcgi.write(obj.toJson().toString, true);\n\t\t} else {\n\t\t\tauto container = presenter.htmlContainer();\n\t\t\tif(addFormLinks) {\n\t\t\t\tstatic if(is(T : CollectionOf!(C), C))\n\t\t\t\tcontainer.appendHtml(`\n\t\t\t\t\t<form>\n\t\t\t\t\t\t<button type=\"submit\" name=\"_method\" value=\"POST\">Create New</button>\n\t\t\t\t\t</form>\n\t\t\t\t`);\n\t\t\t\telse\n\t\t\t\tcontainer.appendHtml(`\n\t\t\t\t\t<a href=\"..\">Back</a>\n\t\t\t\t\t<form>\n\t\t\t\t\t\t<button type=\"submit\" name=\"_method\" value=\"PATCH\">Edit</button>\n\t\t\t\t\t\t<button type=\"submit\" name=\"_method\" value=\"DELETE\">Delete</button>\n\t\t\t\t\t</form>\n\t\t\t\t`);\n\t\t\t}\n\t\t\tcontainer.appendChild(obj.toHtml(presenter));\n\t\t\tcgi.write(container.parentDocument.toString, true);\n\t\t}\n\t}\n\n\t// FIXME: I think I need a set type in here....\n\t// it will be nice to pass sets of members.\n\n\ttry\n\tswitch(cgi.requestMethod) {\n\t\tcase Cgi.RequestMethod.GET:\n\t\t\t// I could prolly use template this parameters in the implementation above for some reflection stuff.\n\t\t\t// sure, it doesn't automatically work in subclasses... but I instantiate here anyway...\n\n\t\t\t// automatic forms here for usable basic auto site from browser.\n\t\t\t// even if the format is json, it could actually send out the links and formats\n\t\t\tswitch(cgi.request(\"_method\", \"GET\")) {\n\t\t\t\tcase \"GET\":\n\t\t\t\t\tstatic if(is(T : CollectionOf!(C), C)) {\n\t\t\t\t\t\tauto results = obj.index();\n\t\t\t\t\t\tif(cgi.request(\"format\", \"html\") == \"html\") {\n\t\t\t\t\t\t\tauto container = presenter.htmlContainer();\n\t\t\t\t\t\t\tauto html = presenter.formatReturnValueAsHtml(results.results);\n\t\t\t\t\t\t\tcontainer.appendHtml(`\n\t\t\t\t\t\t\t\t<form>\n\t\t\t\t\t\t\t\t\t<button type=\"submit\" name=\"_method\" value=\"POST\">Create New</button>\n\t\t\t\t\t\t\t\t</form>\n\t\t\t\t\t\t\t`);\n\n\t\t\t\t\t\t\tcontainer.appendChild(html);\n\t\t\t\t\t\t\tcgi.write(container.parentDocument.toString, true);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcgi.setResponseContentType(\"application/json\");\n\t\t\t\t\t\t\timport arsd.jsvar;\n\t\t\t\t\t\t\tvar json = var.emptyArray;\n\t\t\t\t\t\t\tforeach(r; results.results) {\n\t\t\t\t\t\t\t\tvar o = var.emptyObject;\n\t\t\t\t\t\t\t\tforeach(idx, memberName; __traits(derivedMembers, typeof(r)))\n\t\t\t\t\t\t\t\tstatic if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) {\n\t\t\t\t\t\t\t\t\to[memberName] = __traits(getMember, r, memberName);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tjson ~= o;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcgi.write(json.toJson(), true);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tobj.show(urlId);\n\t\t\t\t\t\twriteObject(true);\n\t\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t\tcase \"PATCH\":\n\t\t\t\t\tobj.load(urlId);\n\t\t\t\tgoto case;\n\t\t\t\tcase \"PUT\":\n\t\t\t\tcase \"POST\":\n\t\t\t\t\t// an editing form for the object\n\t\t\t\t\tauto container = presenter.htmlContainer();\n\t\t\t\t\tstatic if(__traits(compiles, () { auto o = new obj.PostProxy(); })) {\n\t\t\t\t\t\tauto form = (cgi.request(\"_method\") == \"POST\") ? presenter.createAutomaticFormForObject(new obj.PostProxy()) : presenter.createAutomaticFormForObject(obj);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tauto form = presenter.createAutomaticFormForObject(obj);\n\t\t\t\t\t}\n\t\t\t\t\tform.attrs.method = \"POST\";\n\t\t\t\t\tform.setValue(\"_method\", cgi.request(\"_method\", \"GET\"));\n\t\t\t\t\tcontainer.appendChild(form);\n\t\t\t\t\tcgi.write(container.parentDocument.toString(), true);\n\t\t\t\tbreak;\n\t\t\t\tcase \"DELETE\":\n\t\t\t\t\t// FIXME: a delete form for the object (can be phrased \"are you sure?\")\n\t\t\t\t\tauto container = presenter.htmlContainer();\n\t\t\t\t\tcontainer.appendHtml(`\n\t\t\t\t\t\t<form method=\"POST\">\n\t\t\t\t\t\t\tAre you sure you want to delete this item?\n\t\t\t\t\t\t\t<input type=\"hidden\" name=\"_method\" value=\"DELETE\" />\n\t\t\t\t\t\t\t<input type=\"submit\" value=\"Yes, Delete It\" />\n\t\t\t\t\t\t</form>\n\n\t\t\t\t\t`);\n\t\t\t\t\tcgi.write(container.parentDocument.toString(), true);\n\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tcgi.write(\"bad method\\n\", true);\n\t\t\t}\n\t\tbreak;\n\t\tcase Cgi.RequestMethod.POST:\n\t\t\t// this is to allow compatibility with HTML forms\n\t\t\tswitch(cgi.request(\"_method\", \"POST\")) {\n\t\t\t\tcase \"PUT\":\n\t\t\t\t\tgoto PUT;\n\t\t\t\tcase \"PATCH\":\n\t\t\t\t\tgoto PATCH;\n\t\t\t\tcase \"DELETE\":\n\t\t\t\t\tgoto DELETE;\n\t\t\t\tcase \"POST\":\n\t\t\t\t\tstatic if(__traits(compiles, () { auto o = new obj.PostProxy(); })) {\n\t\t\t\t\t\tauto p = new obj.PostProxy();\n\t\t\t\t\t\tvoid specialApplyChanges() {\n\t\t\t\t\t\t\tapplyChangesTemplate(cgi, p);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstring n = p.create(&specialApplyChanges);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstring n = obj.create(&applyChanges);\n\t\t\t\t\t}\n\n\t\t\t\t\tauto newUrl = cgi.scriptName ~ cgi.pathInfo ~ \"/\" ~ n;\n\t\t\t\t\tcgi.setResponseLocation(newUrl);\n\t\t\t\t\tcgi.setResponseStatus(\"201 Created\");\n\t\t\t\t\tcgi.write(`The object has been created.`);\n\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tcgi.write(\"bad method\\n\", true);\n\t\t\t}\n\t\t\t// FIXME this should be valid on the collection, but not the child....\n\t\t\t// 303 See Other\n\t\tbreak;\n\t\tcase Cgi.RequestMethod.PUT:\n\t\tPUT:\n\t\t\tobj.replace(urlId, &applyChanges);\n\t\t\twriteObject(false);\n\t\tbreak;\n\t\tcase Cgi.RequestMethod.PATCH:\n\t\tPATCH:\n\t\t\tobj.update(urlId, &applyChanges, modifiedList);\n\t\t\twriteObject(false);\n\t\tbreak;\n\t\tcase Cgi.RequestMethod.DELETE:\n\t\tDELETE:\n\t\t\tobj.remove(urlId);\n\t\t\tcgi.setResponseStatus(\"204 No Content\");\n\t\tbreak;\n\t\tdefault:\n\t\t\t// FIXME: OPTIONS, HEAD\n\t}\n\tcatch(Throwable t) {\n\t\tpresenter.presentExceptionAsHtml(cgi, t);\n\t}\n\n\treturn true;\n}\n\n/+\nstruct SetOfFields(T) {\n\tprivate void[0][string] storage;\n\tvoid set(string what) {\n\t\t//storage[what] =\n\t}\n\tvoid unset(string what) {}\n\tvoid setAll() {}\n\tvoid unsetAll() {}\n\tbool isPresent(string what) { return false; }\n}\n+/\n\n/+\nenum readonly;\nenum hideonindex;\n+/\n\n/++\n\tReturns true if I recommend gzipping content of this type. You might\n\twant to call it from your Presenter classes before calling cgi.write.\n\n\t---\n\tcgi.setResponseContentType(yourContentType);\n\tcgi.gzipResponse = gzipRecommendedForContentType(yourContentType);\n\tcgi.write(yourData, true);\n\t---\n\n\tThis is used internally by [serveStaticFile], [serveStaticFileDirectory], [serveStaticData], and maybe others I forgot to update this doc about.\n\n\n\tThe implementation considers text content to be recommended to gzip. This may change, but it seems reasonable enough for now.\n\n\tHistory:\n\t\tAdded January 28, 2023 (dub v11.0)\n+/\nbool gzipRecommendedForContentType(string contentType) {\n\tif(contentType.startsWith(\"text/\"))\n\t\treturn true;\n\tif(contentType.startsWith(\"application/javascript\"))\n\t\treturn true;\n\n\treturn false;\n}\n\n/++\n\tServes a static file. To be used with [dispatcher].\n\n\tSee_Also: [serveApi], [serveRestObject], [dispatcher], [serveRedirect]\n+/\nauto serveStaticFile(string urlPrefix, string filename = null, string contentType = null) {\n// https://baus.net/on-tcp_cork/\n// man 2 sendfile\n\tassert(urlPrefix[0] == '/');\n\tif(filename is null)\n\t\tfilename = decodeComponent(urlPrefix[1 .. $]); // FIXME is this actually correct?\n\tif(contentType is null) {\n\t\tcontentType = contentTypeFromFileExtension(filename);\n\t}\n\n\tstatic struct DispatcherDetails {\n\t\tstring filename;\n\t\tstring contentType;\n\t}\n\n\tstatic bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {\n\t\tif(details.contentType.indexOf(\"image/\") == 0 || details.contentType.indexOf(\"audio/\") == 0)\n\t\t\tcgi.setCache(true);\n\t\tcgi.setResponseContentType(details.contentType);\n\t\tcgi.gzipResponse = gzipRecommendedForContentType(details.contentType);\n\t\tcgi.write(std.file.read(details.filename), true);\n\t\treturn true;\n\t}\n\treturn DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType));\n}\n\n/++\n\tServes static data. To be used with [dispatcher].\n\n\tHistory:\n\t\tAdded October 31, 2021\n+/\nauto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) {\n\tassert(urlPrefix[0] == '/');\n\tif(contentType is null) {\n\t\tcontentType = contentTypeFromFileExtension(urlPrefix);\n\t}\n\n\tstatic struct DispatcherDetails {\n\t\timmutable(void)[] data;\n\t\tstring contentType;\n\t}\n\n\tstatic bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {\n\t\tcgi.setCache(true);\n\t\tcgi.setResponseContentType(details.contentType);\n\t\tcgi.write(details.data, true);\n\t\treturn true;\n\t}\n\treturn DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType));\n}\n\nstring contentTypeFromFileExtension(string filename) {\n\t\tif(filename.endsWith(\".png\"))\n\t\t\treturn \"image/png\";\n\t\tif(filename.endsWith(\".apng\"))\n\t\t\treturn \"image/apng\";\n\t\tif(filename.endsWith(\".svg\"))\n\t\t\treturn \"image/svg+xml\";\n\t\tif(filename.endsWith(\".jpg\"))\n\t\t\treturn \"image/jpeg\";\n\t\tif(filename.endsWith(\".html\"))\n\t\t\treturn \"text/html\";\n\t\tif(filename.endsWith(\".css\"))\n\t\t\treturn \"text/css\";\n\t\tif(filename.endsWith(\".js\"))\n\t\t\treturn \"application/javascript\";\n\t\tif(filename.endsWith(\".wasm\"))\n\t\t\treturn \"application/wasm\";\n\t\tif(filename.endsWith(\".mp3\"))\n\t\t\treturn \"audio/mpeg\";\n\t\tif(filename.endsWith(\".pdf\"))\n\t\t\treturn \"application/pdf\";\n\t\treturn null;\n}\n\n/// This serves a directory full of static files, figuring out the content-types from file extensions.\n/// It does not let you to descend into subdirectories (or ascend out of it, of course)\nauto serveStaticFileDirectory(string urlPrefix, string directory = null, bool recursive = false) {\n\tassert(urlPrefix[0] == '/');\n\tassert(urlPrefix[$-1] == '/');\n\n\tstatic struct DispatcherDetails {\n\t\tstring directory;\n\t\tbool recursive;\n\t}\n\n\tif(directory is null)\n\t\tdirectory = urlPrefix[1 .. $];\n\n\tif(directory.length == 0)\n\t\tdirectory = \"./\";\n\n\tassert(directory[$-1] == '/');\n\n\tstatic bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {\n\t\tauto file = decodeComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct\n\n\t\tif(details.recursive) {\n\t\t\t// never allow a backslash since it isn't in a typical url anyway and makes the following checks easier\n\t\t\tif(file.indexOf(\"\\\\\") != -1)\n\t\t\t\treturn false;\n\n\t\t\timport std.path;\n\n\t\t\tfile = std.path.buildNormalizedPath(file);\n\t\t\tenum upOneDir = \"..\" ~ std.path.dirSeparator;\n\n\t\t\t// also no point doing any kind of up directory things since that makes it more likely to break out of the parent\n\t\t\tif(file == \"..\" || file.startsWith(upOneDir))\n\t\t\t\treturn false;\n\t\t\tif(std.path.isAbsolute(file))\n\t\t\t\treturn false;\n\n\t\t\t// FIXME: if it has slashes and stuff, should we redirect to the canonical resource? or what?\n\n\t\t\t// once it passes these filters it is probably ok.\n\t\t} else {\n\t\t\tif(file.indexOf(\"/\") != -1 || file.indexOf(\"\\\\\") != -1)\n\t\t\t\treturn false;\n\t\t}\n\n\t\tauto contentType = contentTypeFromFileExtension(file);\n\n\t\tauto fn = details.directory ~ file;\n\t\tif(std.file.exists(fn)) {\n\t\t\t//if(contentType.indexOf(\"image/\") == 0)\n\t\t\t\t//cgi.setCache(true);\n\t\t\t//else if(contentType.indexOf(\"audio/\") == 0)\n\t\t\t\tcgi.setCache(true);\n\t\t\tcgi.setResponseContentType(contentType);\n\t\t\tcgi.gzipResponse = gzipRecommendedForContentType(contentType);\n\t\t\tcgi.write(std.file.read(fn), true);\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory, recursive));\n}\n\n/++\n\tRedirects one url to another\n\n\tSee_Also: [dispatcher], [serveStaticFile]\n+/\nauto serveRedirect(string urlPrefix, string redirectTo, int code = 303) {\n\tassert(urlPrefix[0] == '/');\n\tstatic struct DispatcherDetails {\n\t\tstring redirectTo;\n\t\tstring code;\n\t}\n\n\tstatic bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {\n\t\tcgi.setResponseLocation(details.redirectTo, true, details.code);\n\t\treturn true;\n\t}\n\n\n\treturn DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(redirectTo, getHttpCodeText(code)));\n}\n\n/// Used exclusively with `dispatchTo`\nstruct DispatcherData(Presenter) {\n\tCgi cgi; /// You can use this cgi object.\n\tPresenter presenter; /// This is the presenter from top level, and will be forwarded to the sub-dispatcher.\n\tsize_t pathInfoStart; /// This is forwarded to the sub-dispatcher. It may be marked private later, or at least read-only.\n}\n\n/++\n\tDispatches the URL to a specific function.\n+/\nauto handleWith(alias handler)(string urlPrefix) {\n\t// cuz I'm too lazy to do it better right now\n\tstatic class Hack : WebObject {\n\t\tstatic import std.traits;\n\t\t@UrlName(\"\")\n\t\tauto handle(std.traits.Parameters!handler args) {\n\t\t\treturn handler(args);\n\t\t}\n\t}\n\n\treturn urlPrefix.serveApiInternal!Hack;\n}\n\n/++\n\tDispatches the URL (and anything under it) to another dispatcher function. The function should look something like this:\n\n\t---\n\tbool other(DD)(DD dd) {\n\t\treturn dd.dispatcher!(\n\t\t\t\"/whatever\".serveRedirect(\"/success\"),\n\t\t\t\"/api/\".serveApi!MyClass\n\t\t);\n\t}\n\t---\n\n\tThe `DD` in there will be an instance of [DispatcherData] which you can inspect, or forward to another dispatcher\n\there. It is a template to account for any Presenter type, so you can do compile-time analysis in your presenters.\n\tOr, of course, you could just use the exact type in your own code.\n\n\tYou return true if you handle the given url, or false if not. Just returning the result of [dispatcher] will do a\n\tgood job.\n\n\n+/\nauto dispatchTo(alias handler)(string urlPrefix) {\n\tassert(urlPrefix[0] == '/');\n\tassert(urlPrefix[$-1] != '/');\n\tstatic bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) {\n\t\treturn handler(DispatcherData!Presenter(cgi, presenter, urlPrefix.length));\n\t}\n\n\treturn DispatcherDefinition!(internalHandler)(urlPrefix, false);\n}\n\n/++\n\tSee [serveStaticFile] if you want to serve a file off disk.\n\n\tHistory:\n\t\tAdded January 28, 2023 (dub v11.0)\n+/\nauto serveStaticData(string urlPrefix, immutable(ubyte)[] data, string contentType, string filenameToSuggestAsDownload = null) {\n\tassert(urlPrefix[0] == '/');\n\n\tstatic struct DispatcherDetails {\n\t\timmutable(ubyte)[] data;\n\t\tstring contentType;\n\t\tstring filenameToSuggestAsDownload;\n\t}\n\n\tstatic bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {\n\t\tcgi.setCache(true);\n\t\tcgi.setResponseContentType(details.contentType);\n\t\tif(details.filenameToSuggestAsDownload.length)\n    \t\t\tcgi.header(\"Content-Disposition: attachment; filename=\\\"\"~details.filenameToSuggestAsDownload~\"\\\"\");\n\t\tcgi.gzipResponse = gzipRecommendedForContentType(details.contentType);\n\t\tcgi.write(details.data, true);\n\t\treturn true;\n\t}\n\treturn DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType, filenameToSuggestAsDownload));\n}\n\n/++\n\tPlaceholder for use with [dispatchSubsection]'s `NewPresenter` argument to indicate you want to keep the parent's presenter.\n\n\tHistory:\n\t\tAdded January 28, 2023 (dub v11.0)\n+/\nalias KeepExistingPresenter = typeof(null);\n\n/++\n\tFor use with [dispatchSubsection]. Calls your filter with the request and if your filter returns false,\n\tthis issues the given errorCode and stops processing.\n\n\t---\n\t\tbool hasAdminPermissions(Cgi cgi) {\n\t\t\treturn true;\n\t\t}\n\n\t\tmixin DispatcherMain!(\n\t\t\t\"/admin\".dispatchSubsection!(\n\t\t\t\tpassFilterOrIssueError!(hasAdminPermissions, 403),\n\t\t\t\tKeepExistingPresenter,\n\t\t\t\t\"/\".serveApi!AdminFunctions\n\t\t\t)\n\t\t);\n\t---\n\n\tHistory:\n\t\tAdded January 28, 2023 (dub v11.0)\n+/\ntemplate passFilterOrIssueError(alias filter, int errorCode) {\n\tbool passFilterOrIssueError(DispatcherDetails)(DispatcherDetails dd) {\n\t\tif(filter(dd.cgi))\n\t\t\treturn true;\n\t\tdd.presenter.renderBasicError(dd.cgi, errorCode);\n\t\treturn false;\n\t}\n}\n\n/++\n\tAllows for a subsection of your dispatched urls to be passed through other a pre-request filter, optionally pick up an new presenter class,\n\tand then be dispatched to their own handlers.\n\n\t---\n\t/+\n\t// a long-form filter function\n\tbool permissionCheck(DispatcherData)(DispatcherData dd) {\n\t\t// you are permitted to call mutable methods on the Cgi object\n\t\t// Note if you use a Cgi subclass, you can try dynamic casting it back to your custom type to attach per-request data\n\t\t// though much of the request is immutable so there's only so much you're allowed to do to modify it.\n\n\t\tif(checkPermissionOnRequest(dd.cgi)) {\n\t\t\treturn true; // OK, allow processing to continue\n\t\t} else {\n\t\t\tdd.presenter.renderBasicError(dd.cgi, 403); // reply forbidden to the requester\n\t\t\treturn false; // and stop further processing into this subsection\n\t\t}\n\t}\n\t+/\n\n\t// but you can also do short-form filters:\n\n\tbool permissionCheck(Cgi cgi) {\n\t\treturn (\"ok\" in cgi.get) !is null;\n\t}\n\n\t// handler for the subsection\n\tclass AdminClass : WebObject {\n\t\tint foo() { return 5; }\n\t}\n\n\t// handler for the main site\n\tclass TheMainSite : WebObject {}\n\n\tmixin DispatcherMain!(\n\t\t\"/admin\".dispatchSubsection!(\n\t\t\t// converts our short-form filter into a long-form filter\n\t\t\tpassFilterOrIssueError!(permissionCheck, 403),\n\t\t\t// can use a new presenter if wanted for the subsection\n\t\t\tKeepExistingPresenter,\n\t\t\t// and then provide child route dispatchers\n\t\t\t\"/\".serveApi!AdminClass\n\t\t),\n\t\t// and back to the top level\n\t\t\"/\".serveApi!TheMainSite\n\t);\n\t---\n\n\tNote you can encapsulate sections in files like this:\n\n\t---\n\tauto adminDispatcher(string urlPrefix) {\n\t\treturn urlPrefix.dispatchSubsection!(\n\t\t\t....\n\t\t);\n\t}\n\n\tmixin DispatcherMain!(\n\t\t\"/admin\".adminDispatcher,\n\t\t// and so on\n\t)\n\t---\n\n\tIf you want no filter, you can pass `(cgi) => true` as the filter to approve all requests.\n\n\tIf you want to keep the same presenter as the parent, use [KeepExistingPresenter] as the presenter argument.\n\n\n\tHistory:\n\t\tAdded January 28, 2023 (dub v11.0)\n+/\nauto dispatchSubsection(alias PreRequestFilter, NewPresenter, definitions...)(string urlPrefix) {\n\tassert(urlPrefix[0] == '/');\n\tassert(urlPrefix[$-1] != '/');\n\tstatic bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) {\n\t\tstatic if(!is(PreRequestFilter == typeof(null))) {\n\t\t\tif(!PreRequestFilter(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)))\n\t\t\t\treturn true; // we handled it by rejecting it\n\t\t}\n\n\t\tstatic if(is(NewPresenter == Presenter) || is(NewPresenter == typeof(null))) {\n\t\t\treturn dispatcher!definitions(DispatcherData!Presenter(cgi, presenter, urlPrefix.length));\n\t\t} else {\n\t\t\tauto newPresenter = new NewPresenter();\n\t\t\treturn dispatcher!(definitions(DispatcherData!NewPresenter(cgi, presenter, urlPrefix.length)));\n\t\t}\n\t}\n\n\treturn DispatcherDefinition!(internalHandler)(urlPrefix, false);\n}\n\n/++\n\tA URL dispatcher.\n\n\t---\n\tif(cgi.dispatcher!(\n\t\t\"/api/\".serveApi!MyApiClass,\n\t\t\"/objects/lol\".serveRestObject!MyRestObject,\n\t\t\"/file.js\".serveStaticFile,\n\t\t\"/admin/\".dispatchTo!adminHandler\n\t)) return;\n\t---\n\n\n\tYou define a series of url prefixes followed by handlers.\n\n\tYou may want to do different pre- and post- processing there, for example,\n\tan authorization check and different page layout. You can use different\n\tpresenters and different function chains. See [dispatchSubsection] for details.\n\n\t[dispatchTo] will send the request to another function for handling.\n+/\ntemplate dispatcher(definitions...) {\n\tbool dispatcher(Presenter)(Cgi cgi, Presenter presenterArg = null) {\n\t\tstatic if(is(Presenter == typeof(null))) {\n\t\t\tstatic class GenericWebPresenter : WebPresenter!(GenericWebPresenter) {}\n\t\t\tauto presenter = new GenericWebPresenter();\n\t\t} else\n\t\t\talias presenter = presenterArg;\n\n\t\treturn dispatcher(DispatcherData!(typeof(presenter))(cgi, presenter, 0));\n\t}\n\n\tbool dispatcher(DispatcherData)(DispatcherData dispatcherData) if(!is(DispatcherData : Cgi)) {\n\t\t// I can prolly make this more efficient later but meh.\n\t\tforeach(definition; definitions) {\n\t\t\tif(definition.rejectFurther) {\n\t\t\t\tif(dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $] == definition.urlPrefix) {\n\t\t\t\t\tauto ret = definition.handler(\n\t\t\t\t\t\tdispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length],\n\t\t\t\t\t\tdispatcherData.cgi, dispatcherData.presenter, definition.details);\n\t\t\t\t\tif(ret)\n\t\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} else if(\n\t\t\t\tdispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $].startsWith(definition.urlPrefix) &&\n\t\t\t\t// cgi.d dispatcher urls must be complete or have a /;\n\t\t\t\t// \"foo\" -> thing should NOT match \"foobar\", just \"foo\" or \"foo/thing\"\n\t\t\t\t(definition.urlPrefix[$-1] == '/' || (dispatcherData.pathInfoStart + definition.urlPrefix.length) == dispatcherData.cgi.pathInfo.length\n\t\t\t\t|| dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart + definition.urlPrefix.length] == '/')\n\t\t\t\t) {\n\t\t\t\tauto ret = definition.handler(\n\t\t\t\t\tdispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length],\n\t\t\t\t\tdispatcherData.cgi, dispatcherData.presenter, definition.details);\n\t\t\t\tif(ret)\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}\n\n});\n\nprivate struct StackBuffer {\n\tchar[1024] initial = void;\n\tchar[] buffer;\n\tsize_t position;\n\n\tthis(int a) {\n\t\tbuffer = initial[];\n\t\tposition = 0;\n\t}\n\n\tvoid add(in char[] what) {\n\t\tif(position + what.length > buffer.length)\n\t\t\tbuffer.length = position + what.length + 1024; // reallocate with GC to handle special cases\n\t\tbuffer[position .. position + what.length] = what[];\n\t\tposition += what.length;\n\t}\n\n\tvoid add(in char[] w1, in char[] w2, in char[] w3 = null) {\n\t\tadd(w1);\n\t\tadd(w2);\n\t\tadd(w3);\n\t}\n\n\tvoid add(long v) {\n\t\tchar[16] buffer = void;\n\t\tauto pos = buffer.length;\n\t\tbool negative;\n\t\tif(v < 0) {\n\t\t\tnegative = true;\n\t\t\tv = -v;\n\t\t}\n\t\tdo {\n\t\t\tbuffer[--pos] = cast(char) (v % 10 + '0');\n\t\t\tv /= 10;\n\t\t} while(v);\n\n\t\tif(negative)\n\t\t\tbuffer[--pos] = '-';\n\n\t\tauto res = buffer[pos .. $];\n\n\t\tadd(res[]);\n\t}\n\n\tchar[] get() @nogc {\n\t\treturn buffer[0 .. position];\n\t}\n}\n\n// duplicated in http2.d\nprivate static string getHttpCodeText(int code) pure nothrow @nogc {\n\tswitch(code) {\n\t\tcase 200: return \"200 OK\";\n\t\tcase 201: return \"201 Created\";\n\t\tcase 202: return \"202 Accepted\";\n\t\tcase 203: return \"203 Non-Authoritative Information\";\n\t\tcase 204: return \"204 No Content\";\n\t\tcase 205: return \"205 Reset Content\";\n\t\tcase 206: return \"206 Partial Content\";\n\t\t//\n\t\tcase 300: return \"300 Multiple Choices\";\n\t\tcase 301: return \"301 Moved Permanently\";\n\t\tcase 302: return \"302 Found\";\n\t\tcase 303: return \"303 See Other\";\n\t\tcase 304: return \"304 Not Modified\";\n\t\tcase 305: return \"305 Use Proxy\";\n\t\tcase 307: return \"307 Temporary Redirect\";\n\t\tcase 308: return \"308 Permanent Redirect\";\n\n\t\t//\n\t\tcase 400: return \"400 Bad Request\";\n\t\tcase 401: return \"401 Unauthorized\";\n\t\tcase 402: return \"402 Payment Required\";\n\t\tcase 403: return \"403 Forbidden\";\n\t\tcase 404: return \"404 Not Found\";\n\t\tcase 405: return \"405 Method Not Allowed\";\n\t\tcase 406: return \"406 Not Acceptable\";\n\t\tcase 407: return \"407 Proxy Authentication Required\";\n\t\tcase 408: return \"408 Request Timeout\";\n\t\tcase 409: return \"409 Conflict\";\n\t\tcase 410: return \"410 Gone\";\n\t\tcase 411: return \"411 Length Required\";\n\t\tcase 412: return \"412 Precondition Failed\";\n\t\tcase 413: return \"413 Payload Too Large\";\n\t\tcase 414: return \"414 URI Too Long\";\n\t\tcase 415: return \"415 Unsupported Media Type\";\n\t\tcase 416: return \"416 Range Not Satisfiable\";\n\t\tcase 417: return \"417 Expectation Failed\";\n\t\tcase 418: return \"418 I'm a teapot\";\n\t\tcase 421: return \"421 Misdirected Request\";\n\t\tcase 422: return \"422 Unprocessable Entity (WebDAV)\";\n\t\tcase 423: return \"423 Locked (WebDAV)\";\n\t\tcase 424: return \"424 Failed Dependency (WebDAV)\";\n\t\tcase 425: return \"425 Too Early\";\n\t\tcase 426: return \"426 Upgrade Required\";\n\t\tcase 428: return \"428 Precondition Required\";\n\t\tcase 431: return \"431 Request Header Fields Too Large\";\n\t\tcase 451: return \"451 Unavailable For Legal Reasons\";\n\n\t\tcase 500: return \"500 Internal Server Error\";\n\t\tcase 501: return \"501 Not Implemented\";\n\t\tcase 502: return \"502 Bad Gateway\";\n\t\tcase 503: return \"503 Service Unavailable\";\n\t\tcase 504: return \"504 Gateway Timeout\";\n\t\tcase 505: return \"505 HTTP Version Not Supported\";\n\t\tcase 506: return \"506 Variant Also Negotiates\";\n\t\tcase 507: return \"507 Insufficient Storage (WebDAV)\";\n\t\tcase 508: return \"508 Loop Detected (WebDAV)\";\n\t\tcase 510: return \"510 Not Extended\";\n\t\tcase 511: return \"511 Network Authentication Required\";\n\t\t//\n\t\tdefault: assert(0, \"Unsupported http code\");\n\t}\n}\n\n\n/+\n/++\n\tThis is the beginnings of my web.d 2.0 - it dispatches web requests to a class object.\n\n\tIt relies on jsvar.d and dom.d.\n\n\n\tYou can get javascript out of it to call. The generated functions need to look\n\tlike\n\n\tfunction name(a,b,c,d,e) {\n\t\treturn _call(\"name\", {\"realName\":a,\"sds\":b});\n\t}\n\n\tAnd _call returns an object you can call or set up or whatever.\n+/\nbool apiDispatcher()(Cgi cgi) {\n\timport arsd.jsvar;\n\timport arsd.dom;\n}\n+/\nversion(linux)\nprivate extern(C) int eventfd (uint initval, int flags) nothrow @trusted @nogc;\n/*\nCopyright: Adam D. Ruppe, 2008 - 2023\nLicense:   [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0].\nAuthors: Adam D. Ruppe\n\n\tCopyright Adam D. Ruppe 2008 - 2023.\nDistributed under the Boost Software License, Version 1.0.\n   (See accompanying file LICENSE_1_0.txt or copy at\n\thttp://www.boost.org/LICENSE_1_0.txt)\n*/\n"
  },
  {
    "path": "src/clientSideFiltering.d",
    "content": "// What is this module called?\nmodule clientSideFiltering;\n\n// What does this module require to function?\nimport std.algorithm;\nimport std.array;\nimport std.file;\nimport std.path;\nimport std.regex;\nimport std.stdio;\nimport std.string;\nimport std.conv;\n\n// What other modules that we have created do we need to import?\nimport config;\nimport util;\nimport log;\n\nclass ClientSideFiltering {\n\t// Class variables\n\tApplicationConfig appConfig;\n\tstring[] syncListRules;\n\tstring[] syncListIncludePathsOnly; // These are 'include' rules that start with a '/'\n\tstring[] syncListAnywherePathOnly; // These are 'include' rules that do not start with a '/', thus are to be searched anywhere for inclusion\n\tRegex!char fileMask;\n\tRegex!char directoryMask;\n\tbool skipDirStrictMatch = false;\n\tbool skipDotfiles = false;\n\t\n\tthis(ApplicationConfig appConfig) {\n\t\t// Configure the class variable to consume the application configuration\n\t\tthis.appConfig = appConfig;\n\t}\n\t\n\t// Initialise the required items\n\tbool initialise() {\n\t\t// Log what is being done\n\t\tif (debugLogging) {addLogEntry(\"Configuring Client Side Filtering (Selective Sync)\", [\"debug\"]);}\n\t\t\n\t\t// Load the sync_list file if it exists\n\t\tif (exists(appConfig.syncListFilePath)){\n\t\t\tloadSyncList(appConfig.syncListFilePath);\n\t\t}\n\t\t\n\t\t// Handle skip_dir configuration in config file\n\t\tif (debugLogging) {addLogEntry(\"Configuring skip_dir ...\", [\"debug\"]);}\n\t\t\n\t\t// Validate skip_dir entries to ensure that this does not contain an invalid configuration\n\t\t// Do not use a skip_dir entry of .* as this will prevent correct searching of local changes to process.\n\t\tforeach(entry; appConfig.getValueString(\"skip_dir\").split(\"|\")){\n\t\t\tif (entry == \".*\") {\n\t\t\t\t// invalid entry element detected\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: Invalid skip_dir entry '.*' detected.\");\n\t\t\t\taddLogEntry(\"       To exclude hidden directories (those starting with '.'), enable the 'skip_dotfiles' configuration option instead of using wildcard patterns.\");\n\t\t\t\taddLogEntry();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// All skip_dir entries are valid\n\t\tif (debugLogging) {addLogEntry(\"skip_dir: \" ~ appConfig.getValueString(\"skip_dir\"), [\"debug\"]);}\n\t\tsetDirMask(appConfig.getValueString(\"skip_dir\"));\n\t\t\n\t\t// Was --skip-dir-strict-match configured?\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Configuring skip_dir_strict_match ...\", [\"debug\"]);\n\t\t\taddLogEntry(\"skip_dir_strict_match: \" ~ to!string(appConfig.getValueBool(\"skip_dir_strict_match\")), [\"debug\"]);\n\t\t}\n\t\tif (appConfig.getValueBool(\"skip_dir_strict_match\")) {\n\t\t\tsetSkipDirStrictMatch();\n\t\t}\n\t\t\n\t\t// Handle skip_file configuration in config file\n\t\tif (debugLogging) {addLogEntry(\"Configuring skip_file ...\", [\"debug\"]);}\n\t\t\n\t\t// Validate skip_file entries to ensure that this does not contain an invalid configuration\n\t\t// Do not use a skip_file entry of .* as this will prevent correct searching of local changes to process.\n\t\tforeach(entry; appConfig.getValueString(\"skip_file\").split(\"|\")){\n\t\t\tif (entry == \".*\") {\n\t\t\t\t// invalid entry element detected\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: Invalid skip_file entry '.*' detected.\");\n\t\t\t\taddLogEntry(\"       To exclude hidden files (those starting with '.'), enable the 'skip_dotfiles' configuration option instead of using wildcard patterns.\");\n\t\t\t\taddLogEntry();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// All skip_file entries are valid\n\t\tif (debugLogging) {addLogEntry(\"skip_file: \" ~ appConfig.getValueString(\"skip_file\"), [\"debug\"]);}\n\t\tsetFileMask(appConfig.getValueString(\"skip_file\"));\n\t\t\n\t\t// Was --skip-dot-files configured?\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Configuring skip_dotfiles ...\", [\"debug\"]);\n\t\t\taddLogEntry(\"skip_dotfiles: \" ~ to!string(appConfig.getValueBool(\"skip_dotfiles\")), [\"debug\"]);\n\t\t}\n\t\tif (appConfig.getValueBool(\"skip_dotfiles\")) {\n\t\t\tsetSkipDotfiles();\n\t\t}\n\n\t\t// Validate 'sync_list' include rules are not shadowed by 'skip_file' entries\n\t\tif (!validateSyncListNotShadowedBySkipFile()) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// Validate 'sync_list' include rules are not shadowed by 'skip_dir' entries\n\t\tif (!validateSyncListNotShadowedBySkipDir()) {\n\t\t\t// The application configuration is invalid .. 'skip_dir' is shadowing paths included by 'sync_list'\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// Client Side Filtering has been configured correctly\n\t\treturn true;\n\t}\n\t\n\t// Shutdown components\n\tvoid shutdown() {\n\t\tsyncListRules = null;\n\t\tsyncListIncludePathsOnly = null;\n\t\tsyncListAnywherePathOnly = null;\n\t\tfileMask = regex(\"\");\n\t\tdirectoryMask = regex(\"\");\n\t}\n\n\t// Load sync_list file if it exists\n\tvoid loadSyncList(string filepath) {\n\t\t// open file as read only\n\t\tauto file = File(filepath, \"r\");\n\t\tauto range = file.byLine();\n\n\t\tscope(exit) {\n\t\t\tfile.close();\n\t\t\tobject.destroy(file);\n\t\t\tobject.destroy(range);\n\t\t}\n\n\t\tscope(failure) {\n\t\t\tfile.close();\n\t\t\tobject.destroy(file);\n\t\t\tobject.destroy(range);\n\t\t}\n\n\t\tforeach (line; range) {\n\t\t\tauto cleanLine = strip(line);\n\n\t\t\t// Skip any line that is empty or just contains whitespace\n\t\t\tif (cleanLine.length == 0) continue;\n\n\t\t\t// Skip comments in file\n\t\t\tif (cleanLine[0] == ';' || cleanLine[0] == '#') continue;\n\n\t\t\t// Invalid exclusion rule patterns\n\t\t\tif (cleanLine == \"!/*\" || cleanLine == \"!/\" || cleanLine == \"-/*\" || cleanLine == \"-/\") {\n\t\t\t\tstring errorMessage = \"ERROR: Invalid sync_list rule '\" ~ to!string(cleanLine) ~ \"' detected. Please read the 'sync_list' documentation.\";\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(errorMessage, [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry();\n\t\t\t\t// do not add this rule\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Legacy include root rule\n\t\t\tif (cleanLine == \"/*\" || cleanLine == \"/\") {\n\t\t\t\tstring errorMessage = \"ERROR: Invalid sync_list rule '\" ~ to!string(cleanLine) ~ \"' detected. Please use 'sync_root_files = \\\"true\\\"' or --sync-root-files option to sync files in the root path.\";\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(errorMessage, [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry();\n\t\t\t\t// do not add this rule\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// './' rule warning\n\t\t\tif ((cleanLine.length > 1) && (cleanLine[0] == '.') && (cleanLine[1] == '/')) {\n\t\t\t\tstring errorMessage = \"ERROR: Invalid sync_list rule '\" ~ to!string(cleanLine) ~ \"' detected. Rule should not start with './' - please fix your 'sync_list' rule.\";\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(errorMessage, [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry();\n\t\t\t\t// do not add this rule\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Normalise the 'sync_list' rule and store\n\t\t\tauto normalisedRulePath = buildNormalizedPath(cleanLine);\n\t\t\tsyncListRules ~= normalisedRulePath;\n\n\t\t\t// Only add the normalised rule to the specific include list if not an exclude rule\n\t\t\tif (cleanLine[0] != '!' && cleanLine[0] != '-') {\n\t\t\t\t// All include rules get added here\n\t\t\t\tsyncListIncludePathsOnly ~= normalisedRulePath;\n\t\t\t\t\n\t\t\t\t// Special case for searching local disk for new data added 'somewhere'\n\t\t\t\tif (cleanLine[0] != '/') {\n\t\t\t\t\t// Rule is an 'anywhere' rule within the 'sync_list'\n\t\t\t\t\tsyncListAnywherePathOnly ~= normalisedRulePath;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Close the file post reading it\n\t\tfile.close();\n\t}\n\n\t// return true or false based on if we have loaded any valid sync_list rules\n\tbool validSyncListRules() {\n\t\t// If empty, will return true\n\t\treturn syncListRules.empty;\n\t}\n\t\n\t// Configure the regex that will be used for 'skip_file'\n\tvoid setFileMask(const(char)[] mask) {\n\t\tfileMask = wild2regex(mask);\n\t\tif (debugLogging) {addLogEntry(\"Selective Sync File Mask: \" ~ to!string(fileMask), [\"debug\"]);}\n\t}\n\n\t// Configure the regex that will be used for 'skip_dir'\n\tvoid setDirMask(const(char)[] dirmask) {\n\t\tdirectoryMask = wild2regex(dirmask);\n\t\tif (debugLogging) {addLogEntry(\"Selective Sync Directory Mask: \" ~ to!string(directoryMask), [\"debug\"]);}\n\t}\n\t\n\t// Configure skipDirStrictMatch if function is called\n\t// By default, skipDirStrictMatch = false;\n\tvoid setSkipDirStrictMatch() {\n\t\tskipDirStrictMatch = true;\n\t}\n\t\n\t// Configure skipDotfiles if function is called\n\t// By default, skipDotfiles = false;\n\tvoid setSkipDotfiles() {\n\t\tskipDotfiles = true;\n\t}\n\t\n\t// return value of skipDotfiles\n\tbool getSkipDotfiles() {\n\t\treturn skipDotfiles;\n\t}\n\t\n\t// Match against 'sync_list' only\n\tbool isPathExcludedViaSyncList(string path) {\n\t\t// Are there 'sync_list' rules to process?\n\t\tif (count(syncListRules) > 0) {\n\t\t\t// Perform 'sync_list' rule testing on the given path\n\t\t\treturn isPathExcluded(path);\n\t\t} else {\n\t\t\t// There are no valid 'sync_list' rules that were loaded\n\t\t\treturn false; // not excluded by 'sync_list'\n\t\t}\n\t}\n\t\n\t// config 'skip_dir' parameter checking\n\tbool isDirNameExcluded(string inputPath) {\n\t\t// Returns true if the inputPath matches a skip_dir config entry (directoryMask)\n\t\t// Returns false if no match\n\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"skip_dir evaluation for: \" ~ inputPath, [\"debug\"]);\n\t\t}\n\n\t\t// Build candidate path variants to cover common inputs:\n\t\t// - \"./Documents/Uni\" (most common from sync engine)\n\t\t// - \"Documents/Uni\"  (relative)\n\t\t// - \"/Documents/Uni\" (user occasionally prefixes with '/')\n\t\tstring name = inputPath;\n\n\t\t// Normalise leading \"./\" to relative\n\t\tif (startsWith(name, \"./\")) {\n\t\t\tname = name[2 .. $];\n\t\t\tif (debugLogging) addLogEntry(\"skip_dir evaluation (normalised inputPath, removed leading './'): \" ~ name, [\"debug\"]);\n\t\t}\n\n\t\t// Create a small set of candidates (avoid duplicates)\n\t\tstring[] candidates;\n\t\tvoid addCandidate(string c) {\n\t\t\tif (c.empty) return;\n\t\t\tforeach (e; candidates) {\n\t\t\t\tif (e == c) return;\n\t\t\t}\n\t\t\tcandidates ~= c;\n\t\t}\n\n\t\taddCandidate(name);\n\n\t\t// If name is rooted, also test relative form\n\t\tif (!name.empty && name[0] == '/') {\n\t\t\taddCandidate(name[1 .. $]);\n\t\t} else {\n\t\t\t// If name is relative, also test rooted form (covers skip_dir rules that were authored with a leading '/')\n\t\t\taddCandidate(\"/\" ~ name);\n\t\t}\n\n\t\t// Also test trailing-slash equivalence for directory roots\n\t\t// (treat \"Documents\" and \"Documents/\" the same, but do not create \"//\")\n\t\tstring[] expanded;\n\t\tforeach (c; candidates) {\n\t\t\texpanded ~= c;\n\t\t\tif (c.length > 1 && c[$ - 1] != '/') {\n\t\t\t\texpanded ~= (c ~ \"/\");\n\t\t\t}\n\t\t}\n\t\tcandidates = expanded;\n\n\t\t// ------------------------------------------------------------\n\t\t// 1) Full-path match first (strict semantics)\n\t\t// ------------------------------------------------------------\n\t\tforeach (c; candidates) {\n\t\t\tif (!c.matchFirst(directoryMask).empty) {\n\t\t\t\tif (debugLogging) addLogEntry(\"skip_dir full-path match: \" ~ c, [\"debug\"]);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// ------------------------------------------------------------\n\t\t// 2) Non-strict mode: test path segments for a match\n\t\t// ------------------------------------------------------------\n\t\tif (!skipDirStrictMatch) {\n\t\t\tif (debugLogging) addLogEntry(\"No Strict Matching Enforced - testing individual path segments\", [\"debug\"]);\n\n\t\t\tforeach (c; candidates) {\n\t\t\t\t// buildNormalizedPath may introduce a leading '/', so we keep it as-is\n\t\t\t\t// and let pathSplitter do its job. We are matching segments, not full paths here.\n\t\t\t\tstring path = buildNormalizedPath(c);\n\n\t\t\t\tif (debugLogging) addLogEntry(\"skip_dir segment test path: \" ~ path, [\"debug\"]);\n\n\t\t\t\tforeach_reverse(seg; pathSplitter(path)) {\n\t\t\t\t\tif (seg == \"/\") continue;\n\n\t\t\t\t\t// seg is a single component (e.g. \"Documents\")\n\t\t\t\t\tif (!seg.matchFirst(directoryMask).empty) {\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"skip_dir segment match: \" ~ seg, [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif (debugLogging) addLogEntry(\"Strict Matching Enforced - no segment testing\", [\"debug\"]);\n\t\t}\n\n\t\t// No match\n\t\treturn false;\n\t}\n\n\t// config file skip_file parameter\n\tbool isFileNameExcluded(string name) {\n\t\t// Does the file name match skip_file config entry?\n\t\t// Returns true if the name matches a skip_file config entry\n\t\t// Returns false if no match\n\t\tif (debugLogging) {addLogEntry(\"skip_file evaluation for: \" ~ name, [\"debug\"]);}\n\t\n\t\t// Try full path match first\n\t\tif (!name.matchFirst(fileMask).empty) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\t// check just the file name\n\t\t\tstring filename = baseName(name);\n\t\t\tif(!filename.matchFirst(fileMask).empty) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\t// no match\n\t\treturn false;\n\t}\n\t\n\t// test if the given path is not included in the allowed syncListRules\n\t// if there are no allowed syncListRules always return false\n\tprivate bool isPathExcluded(string path) {\n\t\t// function variables\n\t\tbool exclude = false;\n\t\tbool excludeExactMatch = false; // will get updated to true, if there is a pattern match to sync_list entry\n\t\tbool excludeParentMatched = false; // will get updated to true, if there is a pattern match to sync_list entry\n\t\tbool finalResult = true; // will get updated to false, if pattern match to sync_list entry\n\t\tbool anywhereRuleMatched = false; // will get updated if the 'anywhere' rule matches\n\t\tbool excludeAnywhereMatched = false; // will get updated if the 'anywhere' rule matches\n\t\tbool wildcardRuleMatched = false; // will get updated if the 'wildcard' rule matches\n\t\tbool excludeWildcardMatched = false; // will get updated if the 'wildcard' rule matches\n\t\tint offset;\n\t\tstring wildcard = \"*\";\n\t\tstring globbing = \"**\";\n\t\t\t\t\n\t\t// always allow the root\n\t\tif (path == \".\") return false;\n\t\t\n\t\t// if there are no allowed syncListRules always return false, meaning path is not excluded\n\t\tif (syncListRules.empty) return false;\n\t\t\n\t\t// To ensure we are checking the 'right' path, build the path\n\t\tpath = buildPath(\"/\", buildNormalizedPath(path));\n\n\t\t// Evaluation start point, in order of what is checked as well\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"******************* SYNC LIST RULES EVALUATION START *******************\", [\"debug\"]);\n\t\t\taddLogEntry(\"Evaluation against 'sync_list' rules for this input path: \" ~ path, [\"debug\"]);\n\t\t\taddLogEntry(\"[S]excludeExactMatch      = \" ~ to!string(excludeExactMatch), [\"debug\"]);\n\t\t\taddLogEntry(\"[S]excludeParentMatched   = \" ~ to!string(excludeParentMatched), [\"debug\"]);\n\t\t\taddLogEntry(\"[S]excludeAnywhereMatched = \" ~ to!string(excludeAnywhereMatched), [\"debug\"]);\n\t\t\taddLogEntry(\"[S]excludeWildcardMatched = \" ~ to!string(excludeWildcardMatched), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Split input path by '/' to create an applicable path segment array\n\t\t// - This is reused below in a number of places\n\t\tstring[] pathSegments = path.strip.split(\"/\").filter!(s => !s.empty).array;\n\t\t\n\t\t// Unless path is an exact match, entire sync_list entries need to be processed to ensure negative matches are also correctly detected\n\t\tforeach (syncListRuleEntry; syncListRules) {\n\n\t\t\t// There are several matches we need to think of here\n\t\t\t// Exclusions:\n\t\t\t//\t\t!foldername/*  \t\t\t\t\t \t\t\t= As there is no preceding '/' (after the !) .. this is a rule that should exclude 'foldername' and all its children ANYWHERE\n\t\t\t//\t\t!*.extension   \t\t\t\t\t \t\t\t= As there is no preceding '/' (after the !) .. this is a rule that should exclude any item that has the specified extension ANYWHERE\n\t\t\t//\t\t!/path/to/foldername/*  \t\t \t\t\t= As there IS a preceding '/' (after the !) .. this is a rule that should exclude this specific path and all its children\n\t\t\t//\t\t!/path/to/foldername/*.extension \t\t\t= 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\n\t\t\t//\t\t!/path/to/foldername/*/specific_target/*\t= As there IS a preceding '/' (after the !) .. this excludes 'specific_target' in any subfolder of '/path/to/foldername/'\n\t\t\t//\n\t\t\t// Inclusions:\n\t\t\t//\t\tfoldername/*  \t\t\t\t\t \t\t\t= As there is no preceding '/' .. this is a rule that should INCLUDE 'foldername' and all its children ANYWHERE\n\t\t\t//\t\t*.extension   \t\t\t\t\t \t\t\t= As there is no preceding '/' .. this is a rule that should INCLUDE any item that has the specified extension ANYWHERE\n\t\t\t//\t\t/path/to/foldername/*  \t\t \t\t\t\t= As there IS a preceding '/' .. this is a rule that should INCLUDE this specific path and all its children\n\t\t\t//\t\t/path/to/foldername/*.extension \t\t\t= As there IS a preceding '/' .. this is a rule that should INCLUDE any item that has the specified extension in this path ONLY\n\t\t\t//\t\t/path/to/foldername/*/specific_target/*\t\t= As there IS a preceding '/' .. this INCLUDES 'specific_target' in any subfolder of '/path/to/foldername/'\n\n\t\t\tif (debugLogging) {addLogEntry(\"------------------------------ NEW RULE --------------------------------\", [\"debug\"]);}\n\t\t\t\n\t\t\t// Is this rule an 'exclude' or 'include' rule?\n\t\t\tbool thisIsAnExcludeRule = false;\n\t\t\t\n\t\t\t// Switch based on first character of rule to determine rule type\n\t\t\tswitch (syncListRuleEntry[0]) {\n\t\t\t\tcase '-':\n\t\t\t\t\t// sync_list path starts with '-', this user wants to exclude this path\n\t\t\t\t\texclude = true; // default exclude\n\t\t\t\t\tthisIsAnExcludeRule = true; // exclude rule\n\t\t\t\t\toffset = 1; // To negate the '-' in the rule entry\n\t\t\t\t\tbreak;\n\t\t\t\tcase '!':\n\t\t\t\t\t// sync_list path starts with '!', this user wants to exclude this path\n\t\t\t\t\texclude = true; // default exclude\n\t\t\t\t\tthisIsAnExcludeRule = true; // exclude rule\n\t\t\t\t\toffset = 1; // To negate the '!' in the rule entry\n\t\t\t\t\tbreak;\n\t\t\t\tcase '/':\n\t\t\t\t\t// sync_list path starts with '/', this user wants to include this path\n\t\t\t\t\t// but a '/' at the start causes matching issues, so use the offset for comparison\n\t\t\t\t\texclude = false; // DO NOT EXCLUDE\n\t\t\t\t\tthisIsAnExcludeRule = false; // INCLUDE rule\n\t\t\t\t\toffset = 0;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\t// no negative pattern, default is to not exclude\n\t\t\t\t\texclude = false; // DO NOT EXCLUDE\n\t\t\t\t\tthisIsAnExcludeRule = false; // INCLUDE rule\n\t\t\t\t\toffset = 0;\n\t\t\t}\n\t\t\t\n\t\t\t// Update syncListRuleEntry to remove the offset\n\t\t\tsyncListRuleEntry = syncListRuleEntry[offset..$];\n\t\t\t\n\t\t\t// What 'sync_list' rule are we comparing against?\n\t\t\tif (thisIsAnExcludeRule) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against EXCLUSION 'sync_list' rule: !\" ~ syncListRuleEntry, [\"debug\"]);}\n\t\t\t} else {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against INCLUSION 'sync_list' rule: \" ~ syncListRuleEntry, [\"debug\"]);}\n\t\t\t}\n\t\t\t\n\t\t\t// Split rule path by '/' to create an applicable path segment array\n\t\t\t// - This is reused below in a number of places\n\t\t\tstring[] ruleSegments = syncListRuleEntry.strip.split(\"/\").filter!(s => !s.empty).array;\n\t\t\t\n\t\t\t// Configure logging rule type\n\t\t\tstring ruleKind = thisIsAnExcludeRule ? \"exclusion rule\" : \"inclusion rule\";\n\t\t\t\n\t\t\t// Is path is an exact match of the 'sync_list' rule, or do the input path segments (directories) match the 'sync_list' rule?\n\t\t\t// wildcard (*) rules are below if we get there, if this rule does not contain a wildcard\n\t\t\tif ((to!string(syncListRuleEntry[0]) == \"/\") && (!canFind(syncListRuleEntry, wildcard))) {\n\t\t\t\n\t\t\t\t// what sort of rule is this - 'exact match' include or exclude rule?\n\t\t\t\tif (debugLogging) {addLogEntry(\"Testing input path against an exact match 'sync_list' \" ~ ruleKind, [\"debug\"]);}\n\t\t\t\n\t\t\t\t// Print rule and input segments for validation during debug\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\" - Calculated Rule Segments: \" ~ to!string(ruleSegments), [\"debug\"]);\n\t\t\t\t\taddLogEntry(\" - Calculated Path Segments: \" ~ to!string(pathSegments), [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Test for exact segment matching of input path to rule\n\t\t\t\tif (exactMatchRuleSegmentsToPathSegments(ruleSegments, pathSegments)) {\n\t\t\t\t\t// EXACT PATH MATCH\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Exact path match with 'sync_list' rule entry\", [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\tif (!thisIsAnExcludeRule) {\n\t\t\t\t\t\t// Include Rule\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: direct match\", [\"debug\"]);}\n\t\t\t\t\t\t// final result\n\t\t\t\t\t\tfinalResult = false;\n\t\t\t\t\t\t// direct match, break and search rules no more given include rule match\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Exclude rule\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: exclusion direct match - path to be excluded\", [\"debug\"]);}\n\t\t\t\t\t\t// flag excludeExactMatch so that a 'wildcard match' will not override this exclude\n\t\t\t\t\t\texcludeExactMatch = true;\n\t\t\t\t\t\texclude = true;\n\t\t\t\t\t\t// final result\n\t\t\t\t\t\tfinalResult = true;\n\t\t\t\t\t\t// dont break here, finish checking other rules\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// NOT an EXACT MATCH, so check the very first path segment\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"No exact path match with 'sync_list' rule entry - checking path segments to verify\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t// - This is so that paths in 'sync_list' as specified as /some path/another path/ actually get included|excluded correctly\n\t\t\t\t\tif (matchFirstSegmentToPathFirstSegment(ruleSegments, pathSegments)) {\n\t\t\t\t\t\t// PARENT ROOT MATCH\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Parent root path match with 'sync_list' rule entry\", [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Does the 'rest' of the input path match?\n\t\t\t\t\t\t// We only need to do this step if the input path has more and 1 segment (the parent folder)\n\t\t\t\t\t\tif (count(pathSegments) > 1) {\n\t\t\t\t\t\t\t// More segments to check, so do a parental path match\n\t\t\t\t\t\t\tif (matchRuleSegmentsToPathSegments(ruleSegments, pathSegments)) {\n\t\t\t\t\t\t\t\t// PARENTAL PATH MATCH\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Parental path match with 'sync_list' rule entry\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t// What sort of rule was this?\n\t\t\t\t\t\t\t\tif (!thisIsAnExcludeRule) {\n\t\t\t\t\t\t\t\t\t// Include Rule\n\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: parental path match\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t// final result\n\t\t\t\t\t\t\t\t\tfinalResult = false;\n\t\t\t\t\t\t\t\t\t// parental path match, break and search rules no more given include rule match\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Exclude rule\n\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: exclusion parental path match - path to be excluded\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\texcludeParentMatched = true;\n\t\t\t\t\t\t\t\t\texclude = true;\n\t\t\t\t\t\t\t\t\t// final result\n\t\t\t\t\t\t\t\t\tfinalResult = true;\n\t\t\t\t\t\t\t\t\t// dont break here, finish checking other rules\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// No more segments to check\n\t\t\t\t\t\t\tif (!thisIsAnExcludeRule) {\n\t\t\t\t\t\t\t\t// Include Rule\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: parent root path match to rule\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t// final result\n\t\t\t\t\t\t\t\tfinalResult = false;\n\t\t\t\t\t\t\t\t// parental path match, break and search rules no more given include rule match\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Exclude rule\n\t\t\t\t\t\t\t\t{addLogEntry(\"Evaluation against 'sync_list' rule result: exclusion parent root path match to rule - path to be excluded\", [\"debug\"]);}\n\t\t\t\t\t\t\t\texcludeParentMatched = true;\n\t\t\t\t\t\t\t\texclude = true;\n\t\t\t\t\t\t\t\t// final result\n\t\t\t\t\t\t\t\tfinalResult = true;\n\t\t\t\t\t\t\t\t// dont break here, finish checking other rules\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No parental path segment match\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"No parental path match with 'sync_list' rule entry - exact path matching not possible\", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// What 'rule' type are we currently testing?\n\t\t\t\tif (!thisIsAnExcludeRule) {\n\t\t\t\t\t// Is the path a parental path match to an include 'sync_list' rule?\n\t\t\t\t\tif (isSyncListPrefixMatch(path)) {\n\t\t\t\t\t\t// PARENTAL PATH MATCH\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"Parental path match with 'sync_list' rule entry (syncListIncludePathsOnly)\", [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"Evaluation against 'sync_list' rule result: parental path match (syncListIncludePathsOnly)\", [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// final result\n\t\t\t\t\t\tfinalResult = false;\n\t\t\t\t\t\t// parental path match, break and search rules no more given include rule match\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Is the 'sync_list' rule an 'anywhere' rule?\n\t\t\t//  EXCLUSION\n\t\t\t//    !foldername/*\n\t\t\t//    !*.extension\n\t\t\t//    !foldername\n\t\t\t//  INCLUSION\n\t\t\t//    foldername/*\n\t\t\t//    *.extension\n\t\t\t//    foldername\n\t\t\tif (to!string(syncListRuleEntry[0]) != \"/\") {\n\t\t\t\t// reset anywhereRuleMatched\n\t\t\t\tanywhereRuleMatched = false; \n\t\t\t\n\t\t\t\t// what sort of rule is this - 'anywhere' include or exclude rule?\n\t\t\t\tif (debugLogging) {addLogEntry(\"Testing input path against an anywhere 'sync_list' \" ~ ruleKind, [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// this is an 'anywhere' rule\n\t\t\t\tstring anywhereRuleStripped;\n\t\t\t\t// If this 'sync_list' rule end in '/*' - if yes, remove it to allow for easier comparison\n\t\t\t\tif (syncListRuleEntry.endsWith(\"/*\")) {\n\t\t\t\t\t// strip '/*' from the end of the rule\n\t\t\t\t\tanywhereRuleStripped = syncListRuleEntry.stripRight(\"/*\");\n\t\t\t\t} else {\n\t\t\t\t\t// keep rule 'as-is'\n\t\t\t\t\tanywhereRuleStripped = syncListRuleEntry;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If the input path is exactly the parent root (single segment) and that segment\n\t\t\t\t// matches the rule's first segment, treat it as a match.\n\t\t\t\tif (!ruleSegments.empty && count(pathSegments) == 1 && matchFirstSegmentToPathFirstSegment(ruleSegments, pathSegments)) {\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\" - anywhere rule 'parent root' MATCH with '\" ~ ruleSegments[0] ~ \"'\", [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\tanywhereRuleMatched = true;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (!anywhereRuleMatched) {\n\t\t\t\t\tif (canFind(path, anywhereRuleStripped)) {\n\t\t\t\t\t\t// we matched the path to the rule\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\" - anywhere rule 'canFind' MATCH\", [\"debug\"]);}\n\t\t\t\t\t\tanywhereRuleMatched = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// no 'canFind' match, try via regex\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\" - anywhere rule 'canFind' NO_MATCH .. trying a regex match\", [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// create regex from 'syncListRuleEntry'\n\t\t\t\t\t\tauto allowedMask = regex(createRegexCompatiblePath(syncListRuleEntry));\n\t\t\t\t\t\t\n\t\t\t\t\t\t// perform regex match attempt\n\t\t\t\t\t\tif (matchAll(path, allowedMask)) {\n\t\t\t\t\t\t\t// we regex matched the path to the rule\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\" - anywhere rule 'matchAll via regex' MATCH\", [\"debug\"]);}\n\t\t\t\t\t\t\tanywhereRuleMatched = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// no match\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\" - anywhere rule 'matchAll via regex' NO_MATCH\", [\"debug\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// is this rule matched?\n\t\t\t\tif (anywhereRuleMatched) {\n\t\t\t\t\t// Is this an exclude rule?\n\t\t\t\t\tif (thisIsAnExcludeRule) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: anywhere rule matched and must be excluded\", [\"debug\"]);}\n\t\t\t\t\t\texcludeAnywhereMatched = true;\n\t\t\t\t\t\texclude = true;\n\t\t\t\t\t\tfinalResult = true;\n\t\t\t\t\t\t// anywhere match, break and search rules no more\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: anywhere rule matched and must be included\", [\"debug\"]);}\n\t\t\t\t\t\tfinalResult = false;\n\t\t\t\t\t\texcludeAnywhereMatched = false;\n\t\t\t\t\t\t// anywhere match, break and search rules no more\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Does the 'sync_list' rule contain a wildcard (*) or globbing (**) reference anywhere in the rule?\n\t\t\t//  EXCLUSION\n\t\t\t//    !/Programming/Projects/Android/**/build/*\n\t\t\t//    !/build/kotlin/*\n\t\t\t//  INCLUSION\n\t\t\t//    /Programming/Projects/Android/**/build/*\n\t\t\t//    /build/kotlin/*\n\t\t\tif (canFind(syncListRuleEntry, wildcard)) {\n\t\t\t\t// A '*' wildcard is in the rule, but we do not know what type of wildcard yet .. \n\t\t\t\t// reset the applicable flag\n\t\t\t\twildcardRuleMatched = false;\n\t\t\t\t\n\t\t\t\t// What sort of rule is this - globbing (**) or wildcard (*)\n\t\t\t\tbool globbingRule = false;\n\t\t\t\tglobbingRule = canFind(syncListRuleEntry, globbing);\n\t\t\t\t\n\t\t\t\t// The sync_list rule contains some sort of wildcard sequence - lets log this correctly as to the rule type we are testing\n\t\t\t\tstring ruleType = globbingRule ? \"globbing (**)\" : \"wildcard (*)\";\n\t\t\t\tif (debugLogging) {addLogEntry(\"Testing input path against a \" ~ ruleType ~ \" 'sync_list' \" ~ ruleKind, [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Does the parents of the input path and rule path match .. meaning we can actually evaluate this wildcard rule against the input path\n\t\t\t\tif (matchFirstSegmentToPathFirstSegment(ruleSegments, pathSegments)) {\n\t\t\t\t\t\n\t\t\t\t\t// Is this a globbing rule (**) or just a single wildcard (*) entries\n\t\t\t\t\tif (globbingRule) {\n\t\t\t\t\t\t// globbing (**) rule processing\n\t\t\t\t\t\t\n\t\t\t\t\t\t// globbing rules can only realistically apply if there are enough path segments for the globbing rule to actually apply\n\t\t\t\t\t\t// otherwise we get a bad match - see:\n\t\t\t\t\t\t// - https://github.com/abraunegg/onedrive/issues/3122\n\t\t\t\t\t\t// - https://github.com/abraunegg/onedrive/issues/3122#issuecomment-2661556789\n\t\t\t\t\t\t\n\t\t\t\t\t\tauto wildcardDepth = firstWildcardDepth(syncListRuleEntry);\n\t\t\t\t\t\tauto pathCount = count(pathSegments);\n\n\t\t\t\t\t\t// Are there enough path segments for this globbing rule to apply?\n\t\t\t\t\t\tif (pathCount < wildcardDepth) {\n\t\t\t\t\t\t\t// there are not enough path segments up to the first wildcard character (*) for this rule to even be applicable\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\" - This sync list globbing rule cannot not be evaluated as the globbing appears beyond the current input path\", [\"debug\"]);}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// There are enough segments in the path and rule to test against this globbing rule\n\t\t\t\t\t\t\tif (matchPathAgainstRule(path, syncListRuleEntry)) {\n\t\t\t\t\t\t\t\t// set the applicable flag\n\t\t\t\t\t\t\t\twildcardRuleMatched = true;\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: globbing pattern match using segment matching\", [\"debug\"]);}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// wildcard (*) rule processing\n\t\t\t\t\t\t// create regex from 'syncListRuleEntry'\n\t\t\t\t\t\tauto allowedMask = regex(createRegexCompatiblePath(syncListRuleEntry));\n\t\t\t\t\t\tif (matchAll(path, allowedMask)) {\n\t\t\t\t\t\t\t// set the applicable flag\n\t\t\t\t\t\t\twildcardRuleMatched = true;\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: wildcard pattern match\", [\"debug\"]);}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// matchAll no match ... try another way just to be sure\n\t\t\t\t\t\t\tif (matchPathAgainstRule(path, syncListRuleEntry)) {\n\t\t\t\t\t\t\t\t// set the applicable flag\n\t\t\t\t\t\t\t\twildcardRuleMatched = true;\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: wildcard pattern match using segment matching\", [\"debug\"]);}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Was the rule matched?\n\t\t\t\t\tif (wildcardRuleMatched) {\n\t\t\t\t\t\t// Is this an exclude rule?\n\t\t\t\t\t\tif (thisIsAnExcludeRule) {\n\t\t\t\t\t\t\t// Yes exclude rule\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: wildcard|globbing rule matched and must be excluded\", [\"debug\"]);}\n\t\t\t\t\t\t\texcludeWildcardMatched = true;\n\t\t\t\t\t\t\texclude = true;\n\t\t\t\t\t\t\tfinalResult = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// include rule\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: wildcard|globbing pattern matched and must be included\", [\"debug\"]);}\n\t\t\t\t\t\t\tfinalResult = false;\n\t\t\t\t\t\t\texcludeWildcardMatched = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: No match to 'sync_list' wildcard|globbing rule\", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// log that parental path in input path does not match the parental path in the rule\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' rule result: No evaluation possible - parental input path does not match 'sync_list' rule\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// debug logging post 'sync_list' rule evaluations\n\t\tif (debugLogging) {\n\t\t\t// Rule evaluation complete\n\t\t\taddLogEntry(\"------------------------------------------------------------------------\", [\"debug\"]);\n\t\t\n\t\t\t// Interim results after checking each 'sync_list' rule against the input path\n\t\t\taddLogEntry(\"[F]excludeExactMatch      = \" ~ to!string(excludeExactMatch), [\"debug\"]);\n\t\t\taddLogEntry(\"[F]excludeParentMatched   = \" ~ to!string(excludeParentMatched), [\"debug\"]);\n\t\t\taddLogEntry(\"[F]excludeAnywhereMatched = \" ~ to!string(excludeAnywhereMatched), [\"debug\"]);\n\t\t\taddLogEntry(\"[F]excludeWildcardMatched = \" ~ to!string(excludeWildcardMatched), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Only force exclusion if an exclusion rule actually matched this path\n\t\tif (excludeExactMatch || excludeParentMatched || excludeAnywhereMatched || excludeWildcardMatched) {\n\t\t\tfinalResult = true;\n\t\t}\n\t\t\n\t\t\n\t\t// Final Result\n\t\tif (finalResult) {\n\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' final result: EXCLUDED as no rule included path\", [\"debug\"]);}\n\t\t} else {\n\t\t\tif (debugLogging) {addLogEntry(\"Evaluation against 'sync_list' final result: included for sync\", [\"debug\"]);}\n\t\t}\n\t\tif (debugLogging) {addLogEntry(\"******************* SYNC LIST RULES EVALUATION END *********************\", [\"debug\"]);}\n\t\treturn finalResult;\n\t}\n\t\n\t// Calculate wildcard character depth in path\n\tint firstWildcardDepth(string syncListRuleEntry) {\n\t\tint depth = 0;\n\t\tforeach (segment; pathSplitter(syncListRuleEntry))\n\t\t{\n\t\t\tif (segment.canFind(\"*\")) // Check for wildcard characters\n\t\t\t\treturn depth;\n\t\t\tdepth++;\n\t\t}\n\t\treturn depth; // No wildcard found should be '0'\n\t}\n\n\t// Create a wildcard regex compatible string based on the sync list rule\n\tstring createRegexCompatiblePath(string regexCompatiblePath) {\n\t\t// Escape all special regex characters that could break regex parsing\n\t\tregexCompatiblePath = escaper(regexCompatiblePath).text;\n\t\t\n\t\t// Restore wildcard (*) support with '.*' to be compatible with function and to match any characters\n\t\tregexCompatiblePath = regexCompatiblePath.replace(\"\\\\*\", \".*\");\n\t\t\n\t\t// Ensure space matches only literal space, not \\s (tabs, etc.)\n\t\tregexCompatiblePath = regexCompatiblePath.replace(\" \", \"\\\\ \");\n\t\t\n\t\t// Return the regex compatible path\n\t\treturn regexCompatiblePath;\n\t}\n\n\t// Create a regex compatible string to match a relevant segment\n\tbool matchSegment(string ruleSegment, string pathSegment) {\n\t\t// Create the required pattern\n\t\tauto pattern = regex(\"^\" ~ createRegexCompatiblePath(ruleSegment) ~ \"$\");\n\t\t// Check if there's a match and return result\n\t\treturn !match(pathSegment, pattern).empty;\n\t}\n\t\n\t// Function to handle path matching when using globbing (**)\n\tbool matchPathAgainstRule(string path, string rule) {\n\t\t// Split both the path and rule into segments\n\t\tauto pathSegments = pathSplitter(path).filter!(s => !s.empty).array;\n\t\tauto ruleSegments = pathSplitter(rule).filter!(s => !s.empty).array;\n\n\t\tbool lastSegmentMatchesRule = false;\n\t\tsize_t i = 0, j = 0;\n\n\t\twhile (i < pathSegments.length && j < ruleSegments.length) {\n\t\t\tif (ruleSegments[j] == \"**\") {\n\t\t\t\tif (j == ruleSegments.length - 1) {\n\t\t\t\t\treturn true; // '**' at the end matches everything\n\t\t\t\t}\n\n\t\t\t\t// Find next matching part after '**'\n\t\t\t\twhile (i < pathSegments.length && !matchSegment(ruleSegments[j + 1], pathSegments[i])) {\n\t\t\t\t\ti++;\n\t\t\t\t}\n\t\t\t\tj++; // Move past the '**' in the rule\n\t\t\t} else {\n\t\t\t\tif (!matchSegment(ruleSegments[j], pathSegments[i])) {\n\t\t\t\t\treturn false;\n\t\t\t\t} else {\n\t\t\t\t\t// increment to next set of values\n\t\t\t\t\ti++;\n\t\t\t\t\tj++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Ensure that we handle the last segments gracefully\n\t\tif (i >= pathSegments.length && j < ruleSegments.length) {\n\t\t\tif (j == ruleSegments.length - 1 && ruleSegments[j] == \"*\") {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (ruleSegments[j - 1] == pathSegments[i - 1]) {\n\t\t\t\tlastSegmentMatchesRule = true;\n\t\t\t}\n\t\t}\n\n\t\treturn j == ruleSegments.length || (j == ruleSegments.length - 1 && ruleSegments[j] == \"**\") || lastSegmentMatchesRule;\n\t}\n\t\n\t// Function to perform an exact match of path segments to rule segments\n\tbool exactMatchRuleSegmentsToPathSegments(string[] ruleSegments, string[] inputSegments) {\n\t\t// If rule has more segments than input, or input has more segments than rule, no match is possible\n\t\tif ((ruleSegments.length > inputSegments.length) || ( inputSegments.length > ruleSegments.length)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Iterate over each segment and compare\n\t\tfor (size_t i = 0; i < ruleSegments.length; ++i) {\n\t\t\tif (ruleSegments[i] != inputSegments[i]) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Mismatch at segment \" ~ to!string(i) ~ \": Rule Segment = \" ~ ruleSegments[i] ~ \", Input Segment = \" ~ inputSegments[i], [\"debug\"]);}\n\t\t\t\treturn false; // Return false if any segment doesn't match\n\t\t\t}\n\t\t}\n\n\t\t// If all segments match, return true\n\t\tif (debugLogging) {addLogEntry(\"All segments matched: Rule Segments = \" ~ to!string(ruleSegments) ~ \", Input Segments = \" ~ to!string(inputSegments), [\"debug\"]);}\n\t\treturn true;\n\t}\n\t\n\t// Function to perform a match of path segments to rule segments\n\tbool matchRuleSegmentsToPathSegments(string[] ruleSegments, string[] inputSegments) {\n\t\tif (debugLogging) {addLogEntry(\"Running matchRuleSegmentsToPathSegments()\", [\"debug\"]);}\n\t\t\n\t\t// If rule has more segments than input, no match is possible\n\t\tif (ruleSegments.length > inputSegments.length) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Compare segments up to the length of the rule path\n\t\treturn equal(ruleSegments, inputSegments[0 .. ruleSegments.length]);\n\t}\n\t\n\t// Function to match the first segment only of the path and rule\n\tbool matchFirstSegmentToPathFirstSegment(string[] ruleSegments, string[] inputSegments) {\n\t\t// Check that both segments are not empty\n\t\tif (ruleSegments.length == 0 || inputSegments.length == 0) {\n\t\t\treturn false; // Return false if either segment array is empty\n\t\t}\n\n\t\t// Compare the first segments only\n\t\treturn equal(ruleSegments[0], inputSegments[0]);\n\t}\n\t\n\t// Test the path for prefix matching an include sync_list rule\n\tbool isSyncListPrefixMatch(string inputPath) {\n\t\t// Ensure inputPath ends with a '/' if not root, to avoid false positives\n\t\tstring inputPrefix = inputPath.endsWith(\"/\") ? inputPath : inputPath ~ \"/\";\n\n\t\tforeach (entry; syncListIncludePathsOnly) {\n\t\t\tstring normalisedEntry = entry;\n\t\t\t\n\t\t\t// If rule ends in '/*', treat it as if the '/*' is not there\n\t\t\tif (normalisedEntry.endsWith(\"/*\")) {\n\t\t\t\tnormalisedEntry = normalisedEntry[0 .. $ - 2]; // remove '/*' for this rule comparison\n\t\t\t}\n\n\t\t\t// Ensure trailing '/' for safe prefix match\n\t\t\tstring entryWithSlash = normalisedEntry.endsWith(\"/\") ? normalisedEntry : normalisedEntry ~ \"/\";\n\n\t\t\t// Match input as being equal to or under the rule path, or rule path being under the input path\n\t\t\tif (entryWithSlash.startsWith(inputPrefix) || inputPrefix.startsWith(entryWithSlash)) {\n\t\t\t\t// Debug the exact 'sync_list' inclusion rule this matched\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"Parental path matched 'sync_list' Inclusion Rule: \" ~ to!string(entry), [\"debug\"]);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\t\n\t// Do any 'anywhere' sync_list' rules exist for inclusion?\n\tbool syncListAnywhereInclusionRulesExist() {\n\t\t// Count the entries in syncListAnywherePathOnly\n\t\tauto anywhereRuleCount = count(syncListAnywherePathOnly);\n\t\t\n\t\tif (anywhereRuleCount > 0) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\t\n\t// Validate that 'sync_list' *include* rules are not rendered non-viable by 'skip_dir' entries.\n\t// If an include rule would be excluded by 'skip_dir' evaluation, it is \"shadowed\" by that entry.\n\tbool validateSyncListNotShadowedBySkipDir() {\n\t\t// No sync_list include rules loaded => nothing to validate\n\t\tif (syncListIncludePathsOnly is null || syncListIncludePathsOnly.empty) return true;\n\n\t\t// No skip_dir configured => nothing to validate\n\t\tif (appConfig.getValueString(\"skip_dir\").empty) return true;\n\n\t\tstring[] shadowedRules;\n\n\t\tforeach (rule; syncListIncludePathsOnly) {\n\t\t\t// syncListIncludePathsOnly should only contain include rules, but be defensive.\n\t\t\tif (rule.empty) continue;\n\t\t\tif (rule[0] == '!' || rule[0] == '-') continue;\n\n\t\t\t// Normalise the rule to match how skip_dir rules are evaluated at runtime.\n\t\t\t// skip_dir entries are relative to sync_dir. sync_list entries may be rooted (start with '/').\n\t\t\tstring candidate = rule;\n\n\t\t\t// Normalise leading \"./\" (defensive)\n\t\t\tif (candidate.length >= 2 && candidate[0 .. 2] == \"./\") {\n\t\t\t\tcandidate = candidate[2 .. $];\n\t\t\t}\n\n\t\t\t// Normalise sync_list rooted includes: \"/Documents\" -> \"Documents\"\n\t\t\tif (candidate.length >= 1 && candidate[0] == '/') {\n\t\t\t\t// Remove only the first '/', sync_list rules are single-rooted relative to sync_dir\n\t\t\t\tcandidate = candidate[1 .. $];\n\t\t\t}\n\n\t\t\tif (candidate.empty) continue;\n\n\t\t\t// Use the *actual* runtime skip_dir evaluation logic (strict/non-strict)\n\t\t\t// so the check matches real behaviour.\n\t\t\tbool shadowed = false;\n\n\t\t\t// Test as-is\n\t\t\tif (isDirNameExcluded(candidate)) {\n\t\t\t\tshadowed = true;\n\t\t\t} else {\n\t\t\t\t// Also test with a trailing slash where appropriate, so:\n\t\t\t\t//   skip_dir = \"Documents/\" correctly shadows sync_list = \"/Documents\"\n\t\t\t\t// (Users often represent a directory root either way.)\n\t\t\t\tif (candidate[$ - 1] != '/') {\n\t\t\t\t\tif (isDirNameExcluded(candidate ~ \"/\")) {\n\t\t\t\t\t\tshadowed = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (shadowed) {\n\t\t\t\tshadowedRules ~= rule;\n\t\t\t}\n\t\t}\n\n\t\tif (!shadowedRules.empty) {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"ERROR: Invalid Client Side Filtering configuration detected.\", [\"info\", \"notify\"]);\n\t\t\taddLogEntry(\"       One or more 'sync_list' inclusion rules are shadowed by 'skip_dir' and will never be viable.\", [\"info\", \"notify\"]);\n\t\t\tforeach (r; shadowedRules) {\n\t\t\t\taddLogEntry(\"       Shadowed 'sync_list' rule: \" ~ r, [\"info\", \"notify\"]);\n\t\t\t}\n\t\t\taddLogEntry(\"       Fix: remove or narrow the conflicting 'skip_dir' entry/entries, or adjust your 'sync_list' rules.\", [\"info\", \"notify\"]);\n\t\t\taddLogEntry(\"       See the 'skip_dir' documentation for correct usage and examples.\", [\"info\", \"notify\"]);\n\t\t\taddLogEntry();\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\t\n\t// Validate that 'sync_list' *include* rules are not rendered non-viable by 'skip_file' entries.\n\t// If an include rule would be excluded by 'skip_file' evaluation, it is \"shadowed\" by that entry.\n\tbool validateSyncListNotShadowedBySkipFile() {\n\t\t// No sync_list include rules loaded => nothing to validate\n\t\tif (syncListIncludePathsOnly is null || syncListIncludePathsOnly.empty) return true;\n\n\t\t// No skip_file configured => nothing to validate\n\t\tif (appConfig.getValueString(\"skip_file\").empty) return true;\n\n\t\tstring[] shadowedRules;\n\n\t\tforeach (rule; syncListIncludePathsOnly) {\n\t\t\t// Defensive: ignore empty or explicitly negative rules\n\t\t\tif (rule.empty) continue;\n\t\t\tif (rule[0] == '!' || rule[0] == '-') continue;\n\n\t\t\t// Only validate file-intent rules:\n\t\t\t// - If it ends with '/', treat as a directory include and do not apply skip_file shadow validation.\n\t\t\t//   (Users commonly include folders; skip_file patterns like '*.tmp' should not invalidate that.)\n\t\t\tif (rule.length > 1 && rule[$ - 1] == '/') continue;\n\n\t\t\t// Normalise the rule to match how skip_file rules are evaluated at runtime.\n\t\t\t// skip_file entries are relative to sync_dir. sync_list entries may be rooted (start with '/').\n\t\t\tstring candidate = rule;\n\n\t\t\t// Normalise leading \"./\" (defensive)\n\t\t\tif (candidate.length >= 2 && candidate[0 .. 2] == \"./\") {\n\t\t\t\tcandidate = candidate[2 .. $];\n\t\t\t}\n\n\t\t\t// Normalise sync_list rooted includes: \"/Documents/file.txt\" -> \"Documents/file.txt\"\n\t\t\tif (candidate.length >= 1 && candidate[0] == '/') {\n\t\t\t\tcandidate = candidate[1 .. $];\n\t\t\t}\n\n\t\t\tif (candidate.empty) continue;\n\n\t\t\t// Use the *actual* runtime skip_file evaluation logic so this check matches real behaviour.\n\t\t\tif (isFileNameExcluded(candidate)) {\n\t\t\t\tshadowedRules ~= rule;\n\t\t\t}\n\t\t}\n\n\t\tif (!shadowedRules.empty) {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"ERROR: Invalid Client Side Filtering configuration detected.\", [\"info\", \"notify\"]);\n\t\t\taddLogEntry(\"       One or more 'sync_list' inclusion rules are shadowed by 'skip_file' and will never be viable.\", [\"info\", \"notify\"]);\n\t\t\tforeach (r; shadowedRules) {\n\t\t\t\taddLogEntry(\"       Shadowed 'sync_list' rule: \" ~ r, [\"info\", \"notify\"]);\n\t\t\t}\n\t\t\taddLogEntry(\"       Fix: remove or narrow the conflicting 'skip_file' entry/entries, or adjust your 'sync_list' rules.\", [\"info\", \"notify\"]);\n\t\t\taddLogEntry(\"       See the 'skip_file' documentation for correct usage and examples.\", [\"info\", \"notify\"]);\n\t\t\taddLogEntry();\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}"
  },
  {
    "path": "src/config.d",
    "content": "// What is this module called?\nmodule config;\n\n// What does this module require to function?\nimport core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;\nimport std.array;\nimport std.stdio;\nimport std.process;\nimport std.regex;\nimport std.string;\nimport std.algorithm;\nimport std.algorithm.searching;\nimport std.algorithm.sorting;\nimport std.file;\nimport std.conv;\nimport std.path;\nimport std.getopt;\nimport std.format;\nimport std.ascii;\nimport std.datetime;\nimport std.exception;\nimport core.sys.posix.unistd : geteuid, getuid;\nimport std.process : spawnProcess, wait;\n\n// What other modules that we have created do we need to import?\nimport log;\nimport util;\n\nclass ApplicationConfig {\n\t// Application default values - these do not change\n\t// - Compile time regex\n\timmutable auto configRegex = ctRegex!(`^(\\w+)\\s*=\\s*\"(.*)\"\\s*$`);\n\t// - Default directory to store data\n\timmutable string defaultSyncDir = \"~/OneDrive\";\n\t// - Default Directory Permissions\n\timmutable long defaultDirectoryPermissionMode = 700;\n\t// - Default File Permissions\n\timmutable long defaultFilePermissionMode = 600;\n\t// - Default types of files to skip\n\t// v2.0.x - 2.4.x: ~*|.~*|*.tmp\n\t// v2.5.x  \t\t : ~*|.~*|*.tmp|*.swp|*.partial\n\timmutable string defaultSkipFile = \"~*|.~*|*.tmp|*.swp|*.partial\";\n\t// - Default directories to skip (default is skip none)\n\timmutable string defaultSkipDir = \"\";\n\t// - Default application logging directory\n\timmutable string defaultLogFileDir = \"/var/log/onedrive\";\n\t// - Default configuration directory\n\timmutable string defaultConfigDirName = \"~/.config/onedrive\";\n\t// - Default 'OneDrive Business Shared Files' Folder Name\n\timmutable string defaultBusinessSharedFilesDirectoryName = \"Files Shared With Me\";\n\t// - Default file fragment size for uploads\n\timmutable long defaultFileFragmentSize = 10; // in MiB\n\timmutable long defaultMaxFileFragmentSize = 60; // in MiB\n\timmutable long defaultMonitorInterval = 300; // 5 minutes\n\t\n\t// Microsoft Requirements \n\t// - Default Application ID (abraunegg)\n\timmutable string defaultApplicationId = \"d50ca740-c83f-4d1b-b616-12c519384f0c\";\n\t\t\n\t// - Microsoft User Agent ISV Tag\n\timmutable string isvTag = \"ISV\";\n\t// - Microsoft User Agent Company name\n\timmutable string companyName = \"abraunegg\";\n\t// - Microsoft Application name as per Microsoft Azure application registration\n\timmutable string appTitle = \"OneDrive Client for Linux\";\n\t// Comply with OneDrive traffic decoration requirements\n\t// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online\n\t// - Identify as ISV and include Company Name, App Name separated by a pipe character and then adding Version number separated with a slash character\n\timmutable string defaultUserAgent = isvTag ~ \"|\" ~ companyName ~ \"|\" ~ appTitle ~ \"/\" ~ strip(import(\"version\"));\n\t\n\t// HTTP Struct items, used for configuring HTTP()\n\t// Curl Timeout Handling\n\t// libcurl dns_cache_timeout timeout\n\timmutable int defaultDnsTimeout = 60; // in seconds\n\t// Connect timeout for HTTP|HTTPS connections\n\t// Controls CURLOPT_CONNECTTIMEOUT\n\timmutable int defaultConnectTimeout = 10; // in seconds\n\t// Default data timeout for HTTP operations\n\t// curl.d has a default of: _defaultDataTimeout = dur!\"minutes\"(2);\n\timmutable int defaultDataTimeout = 60; // in seconds\n\t// Maximum total time (in seconds) that any transfer operation is allowed to take.\n\t// This maps directly to libcurl's CURLOPT_TIMEOUT.\n\t//\n\t// IMPORTANT:\n\t//   • CURLOPT_TIMEOUT applies to the *entire* operation — DNS lookup, TCP connect,\n\t//     TLS negotiation, and the full data transfer.\n\t//   • If this timeout is reached, libcurl will abort the request even if data is\n\t//     flowing normally.\n\t//   • For large file downloads, especially on slower links, setting a non-zero\n\t//     timeout can cause the transfer to be killed prematurely.\n\t//\n\t// Behaviour:\n\t//   • A value of 0 disables the limit entirely (libcurl’s default behaviour).\n\t//   • It is strongly recommended to keep this at 0 unless a hard global cap is\n\t//     explicitly required by the user or their environment.\n\timmutable int defaultOperationTimeout = 0; // 0 = no timeout (safe for extremely large file downloads)\n\t// Specify what IP protocol version should be used when communicating with OneDrive\n\timmutable int defaultIpProtocol = 0; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only\n\t// Specify how many redirects should be allowed\n\timmutable int defaultMaxRedirects = 5;\n\t\t\n\t// Azure Active Directory & Graph Explorer Endpoints\n\t// - Global & Default\n\timmutable string globalAuthEndpoint = \"https://login.microsoftonline.com\";\n\timmutable string globalGraphEndpoint = \"https://graph.microsoft.com\";\n\t// - US Government L4\n\timmutable string usl4AuthEndpoint = \"https://login.microsoftonline.us\";\n\timmutable string usl4GraphEndpoint = \"https://graph.microsoft.us\";\n\t// - US Government L5\n\timmutable string usl5AuthEndpoint = \"https://login.microsoftonline.us\";\n\timmutable string usl5GraphEndpoint = \"https://dod-graph.microsoft.us\";\n\t// - Germany\n\timmutable string deAuthEndpoint = \"https://login.microsoftonline.de\";\n\timmutable string deGraphEndpoint = \"https://graph.microsoft.de\";\n\t// - China\n\timmutable string cnAuthEndpoint = \"https://login.chinacloudapi.cn\";\n\timmutable string cnGraphEndpoint = \"https://microsoftgraph.chinacloudapi.cn\";\n\t\n\t// Application Version\n\timmutable string applicationVersion = \"onedrive \" ~ strip(import(\"version\"));\n\t\n\t// Application items that depend on application run-time environment, thus cannot be immutable\n\t// Public variables\n\t\n\t// Logging verbosity count\n\tlong verbosityCount = 0;\n\t\n\t// Was the application just authorised - paste of response URI\n\tbool applicationAuthoriseResponseURIReceived = false;\n\t\n\t// Store the refreshToken for use within the application\n\tconst(char)[] refreshToken;\n\t// Store the current accessToken for use within the application\n\tconst(char)[] accessToken;\n\t// Store the 'refresh_token' file path\n\tstring refreshTokenFilePath = \"\";\n\t// Store the accessTokenExpiration for use within the application\n\tSysTime accessTokenExpiration;\n\t// Store the 'session_upload.UNIQUE_STRING' file path\n\tstring uploadSessionFilePath = \"\";\n\t// Store the 'resume_download.UNIQUE_STRING' file path\n\tstring resumeDownloadFilePath = \"\";\n\t// Store the Intune account information\n\tstring intuneAccountDetails;\n\t// Store the Intune account information on disk for reuse\n\tstring intuneAccountDetailsFilePath = \"\";\n\t\n\t// API initialisation flags\n\tbool apiWasInitialised = false;\n\tbool syncEngineWasInitialised = false;\n\t\n\t// Important Account Details\n\tstring accountType;\n\tstring defaultDriveId;\n\tstring defaultRootId;\n\t\t\n\t// Sync Operations\n\tbool fullScanTrueUpRequired = false;\n\tbool suppressLoggingOutput = false;\n\t\n\t// WebSocket Operations\n\tbool curlSupportsWebSockets = false;\n\tbool websocketSupportCheckDone = false;\n\tbool websocketNotificationUrlAvailable = false;\n\tstring websocketEndpointResponse;\n\tstring websocketNotificationUrl;\n\tstring websocketUrlExpiry;\n\t\n\t// Default number of concurrent threads when downloading and uploading data\n\tulong defaultConcurrentThreads = 8;\n\t\n\t// Default number of seconds inotify actions will be delayed by\n\tulong defaultInotifyDelay = 5;\n\t\t\n\t// All application run-time paths are formulated from this as a set of defaults\n\t// - What is the home path of the actual 'user' that is running the application\n\tstring defaultHomePath = \"\";\n\t// - What is the config path for the application. By default, this is ~/.config/onedrive but can be overridden by using --confdir\n\tstring configDirName = defaultConfigDirName;\n\t// - In case we have to use a system config directory such as '/etc/onedrive' or similar, store that path in this variable\n\tprivate string systemConfigDirName = \"\";\n\t// - Store the configured converted octal value for directory permissions\n\tprivate int configuredDirectoryPermissionMode;\n\t// - Store the configured converted octal value for file permissions\n\tprivate int configuredFilePermissionMode;\n\t// - Store the 'delta_link' file path\n\tprivate string deltaLinkFilePath = \"\";\n\t// - Store the 'items.sqlite3' file path\n\tstring databaseFilePath = \"\";\n\t// - Store the 'items-dryrun.sqlite3' file path\n\tstring databaseFilePathDryRun = \"\";\n\t// - Store the user 'config' file path\n\tprivate string userConfigFilePath = \"\";\n\t// - Store the system 'config' file path\n\tprivate string systemConfigFilePath = \"\";\n\t// - What is the 'config' file path that will be used?\n\tprivate string applicableConfigFilePath = \"\";\n\t// - Store the 'sync_list' file path\n\tstring syncListFilePath = \"\";\n\t\n\t// OneDrive Business Shared File handling - what directory will be used?\n\tstring configuredBusinessSharedFilesDirectoryName = \"\";\n\n\t// Hash files so that we can detect when the configuration has changed, in items that will require a --resync\n\tprivate string configHashFile = \"\";\n\tprivate string configBackupFile = \"\";\n\tprivate string syncListHashFile = \"\";\n\t\n\t// Store the actual 'runtime' hash\n\tprivate string currentConfigHash = \"\";\n\tprivate string currentSyncListHash = \"\";\n\t\n\t// Store the previous config files hash values (file contents)\n\tprivate string previousConfigHash = \"\";\n\tprivate string previousSyncListHash = \"\";\n\t\t\n\t// Store items that come in from the 'config' file, otherwise these need to be set the defaults\n\tprivate string configFileSyncDir = defaultSyncDir;\n\tprivate string configFileSkipFile = \"\"; // Default for now, if post reading in any user configuration, if still empty, default will be used\n\tprivate bool configFileSkipFileReadIn = false; // If we actually read in something from 'config' file, this gets set to true\n\tprivate string configFileSkipDir = \"\"; // Default here is no directories are skipped\n\tprivate string configFileDriveId = \"\"; // Default here is that no drive id is specified\n\tprivate bool configFileCheckNoSync = false;\n\tprivate bool configFileSkipDotfiles = false;\n\tprivate bool configFileSkipSymbolicLinks = false;\n\tprivate bool configFileSkipSize = false;\n\tprivate bool configFileSyncBusinessSharedItems = false;\n\t\n\t// File permission values (set via initialise function)\n\tprivate int convertedPermissionValue;\n\t\n\t// Array of values that are the actual application runtime configuration\n\t// The values stored in these array's are the actual application configuration which can then be accessed by getValue & setValue\n\tstring[string] stringValues;\n\tlong[string] longValues;\n\tbool[string] boolValues;\n\tbool shellEnvironmentSet = false;\n\t\n\t// GUI Notification Environment variables\n\tbool xdg_exists = false;\n\tbool dbus_exists = false;\n\t\n\t// Recycle Bin Configuration\n\t// These paths are used by the application, if 'use_recycle_bin' is enabled\n\tstring recycleBinParentPath;\n\tstring recycleBinFilePath;\n\tstring recycleBinInfoPath;\n\t\n\t// Runtime 'sync_dir' as initialised\n\tstring runtimeSyncDirectory;\n\t\t\n\t// Initialise the application configuration\n\tbool initialise(string confdirOption, bool helpRequested) {\n\t\t\n\t\t// Default runtime configuration - entries in config file ~/.config/onedrive/config or derived from variables above\n\t\t// 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()\n\t\t// The below becomes the 'default' application configuration before config file and/or cli options are overlaid on top\n\t\t\n\t\t// - Set the required default values\n\t\tstringValues[\"application_id\"] = defaultApplicationId;\n\t\tstringValues[\"log_dir\"] = defaultLogFileDir;\n\t\tstringValues[\"skip_dir\"] = defaultSkipDir;\n\t\tstringValues[\"skip_file\"] = defaultSkipFile;\n\t\tstringValues[\"sync_dir\"] = defaultSyncDir;\n\t\tstringValues[\"user_agent\"] = defaultUserAgent;\n\t\t// - The 'drive_id' is used when we specify a specific OneDrive ID when attempting to sync Shared Folders and SharePoint items\n\t\tstringValues[\"drive_id\"] = \"\";\n\t\t// Support National Azure AD endpoints as per https://docs.microsoft.com/en-us/graph/deployments\n\t\t// By default, if empty, use standard Azure AD URL's\n\t\t// Will support the following options:\n\t\t// - USL4\n\t\t//     AD Endpoint:    https://login.microsoftonline.us\n\t\t//     Graph Endpoint: https://graph.microsoft.us\n\t\t// - USL5\n\t\t//     AD Endpoint:    https://login.microsoftonline.us\n\t\t//     Graph Endpoint: https://dod-graph.microsoft.us\n\t\t// - DE\n\t\t//     AD Endpoint:    https://portal.microsoftazure.de\n\t\t//     Graph Endpoint: \thttps://graph.microsoft.de\n\t\t// - CN\n\t\t//     AD Endpoint:    https://login.chinacloudapi.cn\n\t\t//     Graph Endpoint: \thttps://microsoftgraph.chinacloudapi.cn\n\t\tstringValues[\"azure_ad_endpoint\"] = \"\";\n\t\t// Support single-tenant applications that are not able to use the \"common\" multiplexer\n\t\tstringValues[\"azure_tenant_id\"] = \"\";\n\t\t\n\t\t// Support synchronising files based on user desire\n\t\t// - default = whatever order these came in as, processed essentially FIFO\n\t\t// - size_asc = file size ascending\n\t\t// - size_dsc = file size descending\n\t\t// - name_asc = file name ascending\n\t\t// - name_dsc = file name descending\n\t\tstringValues[\"transfer_order\"] = \"default\";\n\t\t\t\t\n\t\t// Recycle Bin Configuration\n\t\t// Enable|Disable feature\n\t\tboolValues[\"use_recycle_bin\"] = false;\n\t\t// Recycle Bin Folder - empty string as a default\n\t\tstringValues[\"recycle_bin_path\"] = \"\";\n\t\t\n\t\t// - Store how many times was --verbose added\n\t\tlongValues[\"verbose\"] = verbosityCount; \n\t\t// - The amount of time (seconds) between monitor sync loops\n\t\tlongValues[\"monitor_interval\"] = defaultMonitorInterval;\n\t\t// - What size of file should be skipped?\n\t\tlongValues[\"skip_size\"] = 0;\n\t\t// - How many 'loops' when using --monitor, before we print out high frequency recurring items?\n\t\tlongValues[\"monitor_log_frequency\"] = 12;\n\t\t// - Number of N sync runs before performing a full local scan of sync_dir\n\t\t//   By default 12 which means every ~60 minutes a full disk scan of sync_dir will occur \n\t\t//   'monitor_interval' * 'monitor_fullscan_frequency' = 3600 = 1 hour\n\t\tlongValues[\"monitor_fullscan_frequency\"] = 12;\n\t\t// - Number of children in a path that is locally removed which will be classified as a 'big data delete'\n\t\tlongValues[\"classify_as_big_delete\"] = 1000;\n\t\t// - Configure the default folder permission attributes for newly created folders\n\t\tlongValues[\"sync_dir_permissions\"] = defaultDirectoryPermissionMode;\n\t\t// - Configure the default file permission attributes for newly created file\n\t\tlongValues[\"sync_file_permissions\"] = defaultFilePermissionMode;\n\t\t// - Configure download / upload rate limits\n\t\tlongValues[\"rate_limit\"] = 0;\n\t\t// - To ensure we do not fill up the load disk, how much disk space should be reserved by default\n\t\tlongValues[\"space_reservation\"] = 50 * 2^^20; // 50 MB as Bytes\n\t\t// - How large should our file fragments be when uploading as an 'upload session' ?\n\t\tlongValues[\"file_fragment_size\"] = defaultFileFragmentSize; // whole number, treated as MB, will be converted to bytes within performSessionFileUpload(). Default is 10.\n\t\t\n\t\t// HTTPS & CURL Operation Settings\n\t\t// - Maximum time an operation is allowed to take\n\t\t//   This includes dns resolution, connecting, data transfer, etc - controls CURLOPT_TIMEOUT\n\t\t// CURLOPT_TIMEOUT: This option sets the maximum time in seconds that you allow the libcurl transfer operation to take. \n\t\t// 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.\n\t\tlongValues[\"operation_timeout\"] = defaultOperationTimeout;\n\t\t// libcurl dns_cache_timeout timeout\n\t\tlongValues[\"dns_timeout\"] = defaultDnsTimeout;\n\t\t// Timeout for HTTPS connections - controls CURLOPT_CONNECTTIMEOUT\n\t\t// 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.\n\t\tlongValues[\"connect_timeout\"] = defaultConnectTimeout;\n\t\t// Timeout for activity on a HTTPS connection\n\t\tlongValues[\"data_timeout\"] = defaultDataTimeout;\n\t\t// What IP protocol version should be used when communicating with OneDrive\n\t\tlongValues[\"ip_protocol_version\"] = defaultIpProtocol; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only\n\t\t// What is the default age that a curl engine should be left idle for, before being destroyed\n\t\tlongValues[\"max_curl_idle\"] = 120;\n\t\t\n\t\t// Number of concurrent threads\n\t\tlongValues[\"threads\"] = defaultConcurrentThreads; // Default is 8, user can increase to max of 16 or decrease\n\t\t\n\t\t// Do we wish to upload only?\n\t\tboolValues[\"upload_only\"] = false;\n\t\t// Do we need to check for the .nomount file on the mount point?\n\t\tboolValues[\"check_nomount\"] = false;\n\t\t// Do we need to check for the .nosync file anywhere?\n\t\tboolValues[\"check_nosync\"] = false;\n\t\t// Do we wish to download only?\n\t\tboolValues[\"download_only\"] = false;\n\t\t// Do we disable notifications?\n\t\tboolValues[\"disable_notifications\"] = false;\n\t\t// Do we bypass all the download validation? \n\t\t// - This is critically important not to disable, but because of SharePoint 'feature' can be highly desirable to enable\n\t\tboolValues[\"disable_download_validation\"] = false;\n\t\t// Do we bypass all the upload validation? \n\t\t// - This is critically important not to disable, but because of SharePoint 'feature' can be highly desirable to enable\n\t\tboolValues[\"disable_upload_validation\"] = false;\n\t\t// Do we enable logging?\n\t\tboolValues[\"enable_logging\"] = false;\n\t\t// Do we force HTTP 1.1 for connections to the OneDrive API\n\t\t// - By default we use the curl library default, which should be HTTP2 for most operations governed by the OneDrive API\n\t\tboolValues[\"force_http_11\"] = false;\n\t\t// Do we treat the local file system as the source of truth for our data?\n\t\tboolValues[\"local_first\"] = false;\n\t\t// Do we ignore local file deletes, so that all files are retained online?\n\t\tboolValues[\"no_remote_delete\"] = false;\n\t\t// Do we skip symbolic links?\n\t\tboolValues[\"skip_symlinks\"] = false;\n\t\t// Do we enable debugging for all HTTPS flows. Critically important for debugging API issues.\n\t\tboolValues[\"debug_https\"] = false;\n\t\t// Do we skip .files and .folders?\n\t\tboolValues[\"skip_dotfiles\"] = false;\n\t\t// Do we perform a 'dry-run' with no local or remote changes actually being performed?\n\t\tboolValues[\"dry_run\"] = false;\n\t\t// Do we sync all the files in the 'sync_dir' root?\n\t\tboolValues[\"sync_root_files\"] = false;\n\t\t// Do we delete source file after successful transfer?\n\t\tboolValues[\"remove_source_files\"] = false;\n\t\t// Do we delete source folders after successful transfer?\n\t\tboolValues[\"remove_source_folders\"] = false;\n\t\t// Do we perform strict matching for skip_dir?\n\t\tboolValues[\"skip_dir_strict_match\"] = false;\n\t\t// Do we perform a --resync?\n\t\tboolValues[\"resync\"] = false;\n\t\t// 'resync' now needs to be acknowledged based on the 'risk' of using it\n\t\tboolValues[\"resync_auth\"] = false;\n\t\t// Ignore data safety checks and overwrite local data rather than preserve & rename\n\t\t// - This is a config file option ONLY\n\t\tboolValues[\"bypass_data_preservation\"] = false;\n\t\t// Allow enable / disable of the syncing of OneDrive Business Shared items (files & folders) via configuration file\n\t\tboolValues[\"sync_business_shared_items\"] = false;\n\t\t// Log to application output running configuration values\n\t\tboolValues[\"display_running_config\"] = false;\n\t\t// Configure read-only authentication scope\n\t\tboolValues[\"read_only_auth_scope\"] = false;\n\t\t// Flag to cleanup local files when using --download-only\n\t\tboolValues[\"cleanup_local_files\"] = false;\n\t\t// Perform a permanentDelete on deletion activities\n\t\tboolValues[\"permanent_delete\"] = false;\n\t\t\n\t\t// Controls how the application handles the Microsoft SharePoint 'feature' of modifying all PDF, MS Office & HTML files with added XML content post upload\n\t\t// - There are 2 ways to solve this:\n\t\t//     1. Download the modified file immediately after upload as per v2.4.x (default)\n\t\t//     2. Create a new online version of the file, which then contributes to the users 'quota'\n\t\tboolValues[\"create_new_file_version\"] = false;\n\t\t\n\t\t// Some Linux editors (vi|vim|nvim|emacs|LibreOffice) use use a safe file-save strategy designed to avoid data corruption. As such, as part of this Process\n\t\t// they 'track' the last modified timestamp of the 'new' file that they create on file save (regardless of new file, modified file)\n\t\t// If *any* other application in the background then 'updates' this timestamp, these Linux editors complain saying that the file has changed:\n\t\t//\n\t\t// \t\tWARNING: The file has been changed since reading it!!!\n\t\t//\t\tDo you really want to write to it (y/n)?\n\t\t//\n\t\t// This is simply because they are looking at the timestamp and *not* if the content has actually changed .... a poor design on those editors\n\t\t//\n\t\t// This option, when enabled, forces the client to use a 'session' upload, which, when the 'file' is uploaded by the session, this includes the local timestamp of the file\n\t\t// and Microsoft OneDrive should be respecting this timestamp as the timestamp to use|set when storing that file online\n\t\tboolValues[\"force_session_upload\"] = false;\n\t\t\n\t\t// Obsidian Editor has been written in such a way that it is constantly writing each and every keystroke to a file.\n\t\t// Not only is this really bad application behaviour, for this client, this means the application is constantly writing to disk, thus attempting to upload file changes.\n\t\t// Unfortunately Obsidian on Linux does not provide a built-in way to disable atomic saves or switch to a backup-copy method via configuration.\n\t\t// This flag tells the 'onedrive' inotify monitor to 'sleep' for this period of time, so that constant system writes are not creating instant data uploads\n\t\tboolValues[\"delay_inotify_processing\"] = false;\n\t\tlongValues[\"inotify_delay\"] = defaultInotifyDelay; // default of 5 seconds\n\t\t\n\t\t// Webhook Feature Options\n\t\tboolValues[\"webhook_enabled\"] = false;\n\t\tstringValues[\"webhook_public_url\"] = \"\";\n\t\tstringValues[\"webhook_listening_host\"] = \"\";\n\t\tlongValues[\"webhook_listening_port\"] = 8888;\n\t\tlongValues[\"webhook_expiration_interval\"] = 600;\n\t\tlongValues[\"webhook_renewal_interval\"] = 300;\n\t\tlongValues[\"webhook_retry_interval\"] = 60;\n\t\t\n\t\t// WebSocket Feature Options\n\t\tboolValues[\"disable_websocket_support\"] = false;\n\t\t\n\t\t// GUI File Transfer and Deletion Notifications\n\t\tboolValues[\"notify_file_actions\"] = false;\n\t\t\n\t\t// Display file transfer metrics\n\t\t// - Enable the calculation of transfer metrics (duration,speed) for the transfer of a file\n\t\tboolValues[\"display_transfer_metrics\"] = false;\n\t\t\n\t\t// Enable writing extended attributes about a file to xattr values\n\t\t// - file creator\n\t\t// - file last modifier\n\t\tboolValues[\"write_xattr_data\"] = false;\n\n\t\t// Diable setting the permissions for directories and files, using the inherited permissions\n\t\tboolValues[\"disable_permission_set\"] = false;\n\t\t\n\t\t// Use authentication via Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session\n\t\tboolValues[\"use_intune_sso\"] = false;\n\t\t\n\t\t// Use authentication via OAuth2 Device Authorisation Flow\n\t\tboolValues[\"use_device_auth\"] = false;\n\t\t\n\t\t// GUI | Display Manager Integration\n\t\tboolValues[\"display_manager_integration\"] = false;\n\t\t\n\t\t// Disable GitHub Version check\n\t\tboolValues[\"disable_version_check\"] = false;\n\t\t\t\t\n\t\t// EXPAND USERS HOME DIRECTORY\n\t\t// Determine the users home directory.\n\t\t// Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts\n\t\t// Check for HOME environment variable\n\t\tif (environment.get(\"HOME\") != \"\"){\n\t\t\t// Use HOME environment variable\n\t\t\tif (debugLogging) {addLogEntry(\"runtime_environment: HOME environment variable detected, expansion of '~' should be possible\", [\"debug\"]);}\n\t\t\tdefaultHomePath = environment.get(\"HOME\");\n\t\t\tshellEnvironmentSet = true;\n\t\t} else {\n\t\t\tif ((environment.get(\"SHELL\") == \"\") && (environment.get(\"USER\") == \"\")){\n\t\t\t\t// No shell is set or username - observed case when running as systemd service under CentOS 7.x\n\t\t\t\tif (debugLogging) {addLogEntry(\"runtime_environment: No HOME, SHELL or USER environment variable configuration detected. Expansion of '~' not possible\", [\"debug\"]);}\n\t\t\t\tdefaultHomePath = \"/root\";\n\t\t\t\tshellEnvironmentSet = false;\n\t\t\t} else {\n\t\t\t\t// A shell & valid user is set, but no HOME is set, use ~ which can be expanded\n\t\t\t\tif (debugLogging) {addLogEntry(\"runtime_environment: SHELL and USER environment variable detected, expansion of '~' should be possible\", [\"debug\"]);}\n\t\t\t\tdefaultHomePath = \"~\";\n\t\t\t\tshellEnvironmentSet = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Outcome of setting 'defaultHomePath'\n\t\tif (debugLogging) {addLogEntry(\"runtime_environment: Calculated defaultHomePath: \" ~ defaultHomePath, [\"debug\"]);}\n\t\t\n\t\t// Configure the default path for the Recycle Bin\n\t\t// Both GNOME and KDE use '~/.local/share/Trash/' as the default path\n\t\t// ~/.local/share/Trash/\n\t\t// ├── files/   # The actual trashed files\n\t\t// └── info/    # .trashinfo metadata about each file (original path, deletion date)\n\t\tsetValueString(\"recycle_bin_path\", defaultHomePath ~ \"/.local/share/Trash/\");\n\t\trecycleBinParentPath = getValueString(\"recycle_bin_path\");\n\t\t\n\t\t// DEVELOPER OPTIONS\n\t\t// display_memory = true | false\n\t\t//  - It may be desirable to display the memory usage of the application to assist with diagnosing memory issues with the application\n\t\t//  - This is especially beneficial when debugging or performing memory tests with Valgrind\n\t\tboolValues[\"display_memory\"] = false;\n\t\t// monitor_max_loop = long value\n\t\t//  - It may be desirable to, when running in monitor mode, force monitor mode to 'quit' after X number of loops\n\t\t//  - This is especially beneficial when debugging or performing memory tests with Valgrind\n\t\tlongValues[\"monitor_max_loop\"] = 0;\n\t\t// display_sync_options = true | false\n\t\t// - It may be desirable to see what options are being passed into performSync() without enabling the full verbose debug logging\n\t\tboolValues[\"display_sync_options\"] = false;\n\t\t// force_children_scan = true | false\n\t\t// - Force client to use /children rather than /delta to query changes on OneDrive\n\t\t// - This option flags nationalCloudDeployment as true, forcing the client to act like it is using a National Cloud Deployment model\n\t\tboolValues[\"force_children_scan\"] = false;\n\t\t// display_processing_time = true | false\n\t\t// - Enabling this option will add function processing times to the console output\n\t\t// - This then enables tracking of where the application is spending most amount of time when processing data when users have questions re performance\n\t\tboolValues[\"display_processing_time\"] = false;\n\t\t\n\t\t// Function variables\n\t\tstring configDirBase;\n\t\tstring systemConfigDirBase = \"/etc\";\n\t\tbool configurationInitialised = false;\n\t\t\n\t\t// Initialise the application configuration, using the provided --confdir option was passed in\n\t\tif (!confdirOption.empty) {\n\t\t\t// A CLI 'confdir' was passed in\n\t\t\t// Clean up any stray \" .. these should not be there for correct process handling of the configuration option\n\t\t\tconfdirOption = strip(confdirOption,\"\\\"\");\n\t\t\tif (debugLogging) {addLogEntry(\"configDirName: CLI override to set configDirName to: \" ~ confdirOption, [\"debug\"]);}\n\t\t\t\n\t\t\t// For the passed in --confdir option ..\n\t\t\tif (canFind(confdirOption,\"~\")) {\n\t\t\t\t// A ~ was found\n\t\t\t\tif (debugLogging) {addLogEntry(\"configDirName: A '~' was found in configDirName, using the calculated 'defaultHomePath' to replace '~'\", [\"debug\"]);}\n\t\t\t\tconfigDirName = defaultHomePath ~ strip(confdirOption,\"~\",\"~\");\n\t\t\t} else {\n\t\t\t\tconfigDirName = confdirOption;\n\t\t\t}\n\t\t} else {\n\t\t\t// Determine the base directory relative to which user specific configuration files should be stored\n\t\t\tif (environment.get(\"XDG_CONFIG_HOME\") != \"\"){\n\t\t\t\tif (debugLogging) {addLogEntry(\"configDirBase: XDG_CONFIG_HOME environment variable set\", [\"debug\"]);}\n\t\t\t\tconfigDirBase = environment.get(\"XDG_CONFIG_HOME\");\n\t\t\t} else {\n\t\t\t\t// XDG_CONFIG_HOME does not exist on systems where X11 is not present - ie - headless systems / servers\n\t\t\t\tif (debugLogging) {addLogEntry(\"configDirBase: WARNING - no XDG_CONFIG_HOME environment variable set\", [\"debug\"]);}\n\t\t\t\tconfigDirBase = buildNormalizedPath(buildPath(defaultHomePath, \".config\"));\n\t\t\t}\n\t\t\t\n\t\t\t// Output configDirBase calculation\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"configDirBase: \" ~ configDirBase, [\"debug\"]);\n\t\t\t\t// Set the calculated application configuration directory\n\t\t\t\taddLogEntry(\"configDirName: Configuring application to use calculated config path\", [\"debug\"]);\n\t\t\t}\n\t\t\t// configDirBase contains the correct path so we do not need to check for presence of '~'\n\t\t\tconfigDirName = buildNormalizedPath(buildPath(configDirBase, \"onedrive\"));\n\t\t}\n\t\t\n\t\t// systemConfigDirBase contains the correct path, build the correct path for the system config file\n\t\tsystemConfigDirName = buildNormalizedPath(buildPath(systemConfigDirBase, \"onedrive\"));\n\t\t\n\t\t// Configuration directory should now have been correctly identified\n\t\tif (!exists(configDirName)) {\n\t\t\t// Attempt path creation\n\t\t\ttry {\n\t\t\t\t// create the configuration directory\n\t\t\t\tmkdirRecurse(configDirName);\n\t\t\t\t// Configure the applicable permissions for the folder\n\t\t\t\tconfigDirName.setAttributes(returnRequiredDirectoryPermissions());\n\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t// Creating the configuration directory failed\n\t\t\t\taddLogEntry(\"ERROR: Unable to create the required application configuration directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\treturn EXIT_FAILURE;\n\t\t\t}\n\t\t} else {\n\t\t\t// The config path exists\n\t\t\t// The path that exists must be a directory, not a file\n\t\t\tif (!isDir(configDirName)) {\n\t\t\t\tif (!confdirOption.empty) {\n\t\t\t\t\t// the configuration path was passed in by the user .. user error\n\t\t\t\t\taddLogEntry(\"ERROR: --confdir entered value is an existing file instead of an existing directory\");\n\t\t\t\t} else {\n\t\t\t\t\t// other error\n\t\t\t\t\taddLogEntry(\"ERROR: \" ~ confdirOption ~ \" is a file rather than a directory\");\n\t\t\t\t}\n\t\t\t\t// Must exit\n\t\t\t\texit(EXIT_FAILURE);\t\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Update application set variables based on configDirName\n\t\t// - What is the full path for the 'refresh_token'\n\t\trefreshTokenFilePath = buildNormalizedPath(buildPath(configDirName, \"refresh_token\"));\n\t\t// - What is the full path for the 'intune_account'\n\t\tintuneAccountDetailsFilePath = buildNormalizedPath(buildPath(configDirName, \"intune_account\"));\n\t\t// - What is the full path for the 'delta_link'\n\t\tdeltaLinkFilePath = buildNormalizedPath(buildPath(configDirName, \"delta_link\"));\n\t\t// - What is the full path for the 'items.sqlite3' - the database cache file\n\t\tdatabaseFilePath = buildNormalizedPath(buildPath(configDirName, \"items.sqlite3\"));\n\t\t// - What is the full path for the 'items-dryrun.sqlite3' - the dry-run database cache file\n\t\tdatabaseFilePathDryRun = buildNormalizedPath(buildPath(configDirName, \"items-dryrun.sqlite3\"));\n\t\t// - What is the full path for the 'resume_upload'\n\t\tuploadSessionFilePath = buildNormalizedPath(buildPath(configDirName, \"session_upload\"));\n\t\t// - What is the full path for the resume 'resume_download' file\n\t\tresumeDownloadFilePath = buildNormalizedPath(buildPath(configDirName, \"resume_download\"));\n\t\t// - What is the full path for the 'sync_list' file\n\t\tsyncListFilePath = buildNormalizedPath(buildPath(configDirName, \"sync_list\"));\n\t\t// - What is the full path for the 'config' - the user file to configure the application\n\t\tuserConfigFilePath = buildNormalizedPath(buildPath(configDirName, \"config\"));\n\t\t// - What is the full path for the system 'config' file if it is required\n\t\tsystemConfigFilePath = buildNormalizedPath(buildPath(systemConfigDirName, \"config\"));\n\t\t\n\t\t// 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\n\t\t// - 'config.backup' file\n\t\t// - applicable 'config' file\n\t\t// - 'sync_list' file\n\t\t// - 'business_shared_items' file\n\t\tconfigBackupFile = buildNormalizedPath(buildPath(configDirName, \".config.backup\"));\n\t\tconfigHashFile = buildNormalizedPath(buildPath(configDirName, \".config.hash\"));\n\t\tsyncListHashFile = buildNormalizedPath(buildPath(configDirName, \".sync_list.hash\"));\n\t\t\t\t\t\t\n\t\t// Debug Output for application set variables based on configDirName\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"refreshTokenFilePath =         \" ~ refreshTokenFilePath, [\"debug\"]);\n\t\t\taddLogEntry(\"intuneAccountDetailsFilePath = \" ~ intuneAccountDetailsFilePath, [\"debug\"]);\n\t\t\taddLogEntry(\"deltaLinkFilePath =            \" ~ deltaLinkFilePath, [\"debug\"]);\n\t\t\taddLogEntry(\"databaseFilePath =             \" ~ databaseFilePath, [\"debug\"]);\n\t\t\taddLogEntry(\"databaseFilePathDryRun =       \" ~ databaseFilePathDryRun, [\"debug\"]);\n\t\t\taddLogEntry(\"uploadSessionFilePath =        \" ~ uploadSessionFilePath, [\"debug\"]);\n\t\t\taddLogEntry(\"userConfigFilePath =           \" ~ userConfigFilePath, [\"debug\"]);\n\t\t\taddLogEntry(\"syncListFilePath =             \" ~ syncListFilePath, [\"debug\"]);\n\t\t\taddLogEntry(\"systemConfigFilePath =         \" ~ systemConfigFilePath, [\"debug\"]);\n\t\t\taddLogEntry(\"configBackupFile =             \" ~ configBackupFile, [\"debug\"]);\n\t\t\taddLogEntry(\"configHashFile =               \" ~ configHashFile, [\"debug\"]);\n\t\t\taddLogEntry(\"syncListHashFile =             \" ~ syncListHashFile, [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Configure the Hash and Backup File Permission Value\n\t\tstring valueToConvert = to!string(defaultFilePermissionMode);\n\t\tauto convertedValue = parse!long(valueToConvert, 8);\n\t\tconvertedPermissionValue = to!int(convertedValue);\n\t\t\n\t\t// Do not try and load any user configuration file if --help was used\n\t\tif (helpRequested) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\t// Initialise the application using the configuration file if it exists\n\t\t\tif (!exists(userConfigFilePath)) {\n\t\t\t\t// 'user' configuration file does not exist .. but did the user specify a custom configuration directory via --confdir ?\n\t\t\t\tif (confdirOption.empty) {\n\t\t\t\t\t// No --confdir entry\n\t\t\t\t\t// Is there a system configuration file?\n\t\t\t\t\tif (!exists(systemConfigFilePath)) {\n\t\t\t\t\t\t// 'system' configuration file does not exist\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"No user or system config file found, using application defaults\", [\"verbose\"]);}\n\t\t\t\t\t\tapplicableConfigFilePath = userConfigFilePath;\n\t\t\t\t\t\tconfigurationInitialised = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// 'system' configuration file exists\n\t\t\t\t\t\t// can we load the configuration file without error?\n\t\t\t\t\t\tif (loadConfigFile(systemConfigFilePath)) {\n\t\t\t\t\t\t\t// configuration file loaded without error\n\t\t\t\t\t\t\taddLogEntry(\"System configuration file successfully loaded\");\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Set 'applicableConfigFilePath' to equal the 'config' we loaded\n\t\t\t\t\t\t\tapplicableConfigFilePath = systemConfigFilePath;\n\t\t\t\t\t\t\t// Update the configHashFile path value to ensure we are using the system 'config' file for the hash\n\t\t\t\t\t\t\tconfigHashFile = buildNormalizedPath(buildPath(systemConfigDirName, \".config.hash\"));\n\t\t\t\t\t\t\tconfigurationInitialised = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// there was a problem loading the configuration file\n\t\t\t\t\t\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\t\t\t\t\t\taddLogEntry(\"System configuration file has errors - please check your configuration\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Set 'applicableConfigFilePath' to equal the 'config' path specified via --confdir\n\t\t\t\t\tapplicableConfigFilePath = userConfigFilePath;\n\t\t\t\t\tconfigurationInitialised = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 'user' configuration file exists in the specified path\n\t\t\t\t// can we load the configuration file without error?\n\t\t\t\tif (loadConfigFile(userConfigFilePath)) {\n\t\t\t\t\t// configuration file loaded without error\n\t\t\t\t\taddLogEntry(\"Configuration file successfully loaded\");\n\t\t\t\t\t\n\t\t\t\t\t// Set 'applicableConfigFilePath' to equal the 'config' we loaded\n\t\t\t\t\tapplicableConfigFilePath = userConfigFilePath;\n\t\t\t\t\tconfigurationInitialised = true;\n\t\t\t\t} else {\n\t\t\t\t\t// there was a problem loading the configuration file\n\t\t\t\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\t\t\t\taddLogEntry(\"Configuration file has errors - please check your configuration\");\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Advise the user path that we will use for the application state data\n\t\t\tif (canFind(applicableConfigFilePath, configDirName)) {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Using 'user' configuration path for application config and state data: \" ~ configDirName, [\"verbose\"]);}\n\t\t\t} else {\t\t\t\t\n\t\t\t\tif (canFind(applicableConfigFilePath, systemConfigDirName)) {\n\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\taddLogEntry(\"Using 'system' configuration path for application config data: \" ~ systemConfigDirName, [\"verbose\"]);\n\t\t\t\t\t\taddLogEntry(\"Using 'user' configuration path for application state data:    \" ~ configDirName, [\"verbose\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// return if the configuration was initialised\n\t\treturn configurationInitialised;\n\t}\n\t\n\t// Create a backup of the 'config' file if it does not exist\n\tvoid createBackupConfigFile() {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\tif (!getValueBool(\"dry_run\")) {\n\t\t\t// Is there a backup of the config file if the config file exists?\n\t\t\tif (exists(applicableConfigFilePath)) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Creating a backup of the applicable config file\", [\"debug\"]);}\n\t\t\t\t// create backup copy of current config file\n\t\t\t\ttry {\n\t\t\t\t\tstd.file.copy(applicableConfigFilePath, configBackupFile);\n\t\t\t\t\t// File Copy should only be readable by the user who created it - 0600 permissions needed\n\t\t\t\t\tconfigBackupFile.setAttributes(convertedPermissionValue);\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// filesystem error\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, configBackupFile, FsErrorSeverity.warning);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// --dry-run scenario ... technically we should not be making any local file changes .......\n\t\t\taddLogEntry(\"DRY-RUN: Not creating backup config file as --dry-run has been used\");\n\t\t}\n\t}\n\t\n\t// Return a given string value based on the provided key\n\tstring getValueString(string key) {\n\t\tauto p = key in stringValues;\n\t\tif (p) {\n\t\t\treturn *p;\n\t\t} else {\n\t\t\tthrow new Exception(\"Missing config value: \" ~ key);\n\t\t}\n\t}\n\n\t// Return a given long value based on the provided key\n\tlong getValueLong(string key) {\n\t\tauto p = key in longValues;\n\t\tif (p) {\n\t\t\treturn *p;\n\t\t} else {\n\t\t\tthrow new Exception(\"Missing config value: \" ~ key);\n\t\t}\n\t}\n\n\t// Return a given bool value based on the provided key\n\tbool getValueBool(string key) {\n\t\tauto p = key in boolValues;\n\t\tif (p) {\n\t\t\treturn *p;\n\t\t} else {\n\t\t\tthrow new Exception(\"Missing config value: \" ~ key);\n\t\t}\n\t}\n\t\n\t// Set a given string value based on the provided key\n\tvoid setValueString(string key, string value) {\n\t\tstringValues[key] = value;\n\t}\n\n\t// Set a given long value based on the provided key\n\tvoid setValueLong(string key, long value) {\n\t\tlongValues[key] = value;\n\t}\n\n\t// Set a given long value based on the provided key\n\tvoid setValueBool(string key, bool value) {\n\t\tboolValues[key] = value;\n\t}\n\t\n\t// Configure the directory octal permission value\n\tvoid configureRequiredDirectoryPermissions() {\n\t\t// return the directory permission mode required\n\t\t// - return octal!defaultDirectoryPermissionMode; ... cant be used .. which is odd\n\t\t// Error: variable defaultDirectoryPermissionMode cannot be read at compile time\n\t\tif (getValueLong(\"sync_dir_permissions\") != defaultDirectoryPermissionMode) {\n\t\t\t// return user configured permissions as octal integer\n\t\t\tstring valueToConvert = to!string(getValueLong(\"sync_dir_permissions\"));\n\t\t\tauto convertedValue = parse!long(valueToConvert, 8);\n\t\t\tconfiguredDirectoryPermissionMode = to!int(convertedValue);\n\t\t} else {\n\t\t\t// return default as octal integer\n\t\t\tstring valueToConvert = to!string(defaultDirectoryPermissionMode);\n\t\t\tauto convertedValue = parse!long(valueToConvert, 8);\n\t\t\tconfiguredDirectoryPermissionMode = to!int(convertedValue);\n\t\t}\n\t}\n\n\t// Configure the file octal permission value\n\tvoid configureRequiredFilePermissions() {\n\t\t// return the file permission mode required\n\t\t// - return octal!defaultFilePermissionMode; ... cant be used .. which is odd\n\t\t// Error: variable defaultFilePermissionMode cannot be read at compile time\n\t\tif (getValueLong(\"sync_file_permissions\") != defaultFilePermissionMode) {\n\t\t\t// return user configured permissions as octal integer\n\t\t\tstring valueToConvert = to!string(getValueLong(\"sync_file_permissions\"));\n\t\t\tauto convertedValue = parse!long(valueToConvert, 8);\n\t\t\tconfiguredFilePermissionMode = to!int(convertedValue);\n\t\t} else {\n\t\t\t// return default as octal integer\n\t\t\tstring valueToConvert = to!string(defaultFilePermissionMode);\n\t\t\tauto convertedValue = parse!long(valueToConvert, 8);\n\t\t\tconfiguredFilePermissionMode = to!int(convertedValue);\n\t\t}\n\t}\n\n\t// Read the configuredDirectoryPermissionMode and return\n\tint returnRequiredDirectoryPermissions() {\n\t\tif (configuredDirectoryPermissionMode == 0) {\n\t\t\t// the configured value is zero, this means that directories would get\n\t\t\t// values of d---------\n\t\t\tconfigureRequiredDirectoryPermissions();\n\t\t}\n\t\treturn configuredDirectoryPermissionMode;\n\t}\n\n\t// Read the configuredFilePermissionMode and return\n\tint returnRequiredFilePermissions() {\n\t\tif (configuredFilePermissionMode == 0) {\n\t\t\t// the configured value is zero\n\t\t\tconfigureRequiredFilePermissions();\n\t\t}\n\t\treturn configuredFilePermissionMode;\n\t}\n\t\n\t// Set file permissions for 'refresh_token' and 'intune_account' to 0600\n\tint returnSecureFilePermission() {\n\t\tstring valueToConvert = to!string(defaultFilePermissionMode);\n\t\tauto convertedValue = parse!long(valueToConvert, 8);\n\t\treturn to!int(convertedValue);\n\t}\n\t\n\t// Load a configuration file from the provided filename\n\tprivate bool loadConfigFile(string filename) {\n\t\ttry {\n\t\t\taddLogEntry(\"Reading configuration file: \" ~ filename);\n\t\t\treadText(filename);\n\t\t} catch (std.file.FileException e) {\n\t\t\taddLogEntry(\"ERROR: Unable to access \" ~ e.msg);\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\tauto file = File(filename, \"r\");\n\t\tstring lineBuffer;\n\t\t\n\t\tscope(exit) {\n\t\t\tfile.close();\n\t\t\tobject.destroy(file);\n\t\t\tobject.destroy(lineBuffer);\n\t\t}\n\t\t\n\t\tscope(failure) {\n\t\t\tfile.close();\n\t\t\tobject.destroy(file);\n\t\t\tobject.destroy(lineBuffer);\n\t\t}\n\t\t\n\t\tforeach (line; file.byLine()) {\n\t\t\tlineBuffer = stripLeft(line).to!string;\n\t\t\tif (lineBuffer.empty || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue;\n\t\t\tauto c = lineBuffer.matchFirst(configRegex);\n\t\t\tif (c.empty) {\n\t\t\t\taddLogEntry(\"Malformed config line: \" ~ lineBuffer);\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"Please review the documentation on how to correctly configure this application.\");\n\t\t\t\tforceExit();\n\t\t\t}\n\n\t\t\tc.popFront(); // skip the whole match\n\t\t\tstring key = c.front.dup;\n\t\t\tc.popFront();\n\n\t\t\t// Handle deprecated keys\n\t\t\tswitch (key) {\n\t\t\t\tcase \"min_notify_changes\":\n\t\t\t\tcase \"force_http_2\":\n\t\t\t\t\taddLogEntry(\"The option '\" ~ key ~ \"' has been deprecated and will be ignored. Please read the updated documentation and update your client configuration to remove this option.\");\n\t\t\t\t\tcontinue;\n\t\t\t\tcase \"sync_business_shared_folders\":\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\taddLogEntry(\"The option 'sync_business_shared_folders' has been deprecated and the process for synchronising Microsoft OneDrive Business Shared Folders has changed.\");\n\t\t\t\t\taddLogEntry(\"Please review the revised documentation on how to correctly configure this application feature.\");\n\t\t\t\t\taddLogEntry(\"You must update your client configuration and make changes to your local filesystem and online data to use this capability.\");\n\t\t\t\t\treturn false;\n\t\t\t\tdefault:\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Process other keys\n\t\t\tif (key in boolValues) {\n\t\t\t\t// Strip quotes and whitespace\n\t\t\t\tstring rawValue = to!string(c.front.dup);\n\t\t\t\t// Evaluate rawValue\n\t\t\t\tif (rawValue == \"true\") {\n\t\t\t\t\tsetValueBool(key, true);\n\t\t\t\t\t// Additional config-specific flags for specific keys\n\t\t\t\t\tif (key == \"check_nosync\") configFileCheckNoSync = true;\n\t\t\t\t\tif (key == \"skip_dotfiles\") configFileSkipDotfiles = true;\n\t\t\t\t\tif (key == \"skip_symlinks\") configFileSkipSymbolicLinks = true;\n\t\t\t\t\tif (key == \"sync_business_shared_items\") configFileSyncBusinessSharedItems = true;\n\t\t\t\t} else if (rawValue == \"false\") {\n\t\t\t\t\tsetValueBool(key, false);\n\t\t\t\t} else {\n\t\t\t\t\taddLogEntry(\"Invalid boolean value for key in config file: \" ~ key ~ \" = \" ~ to!string(c.front.dup));\n\t\t\t\t\taddLogEntry(\"ERROR: Only 'true' or 'false' are accepted for this setting.\");\n\t\t\t\t\tforceExit();\n\t\t\t\t}\n\t\t\t} else if (key in stringValues) {\n\t\t\t\tstring value = c.front.dup;\n\t\t\t\tsetValueString(key, value);\n\t\t\t\tif (key == \"sync_dir\") {\n\t\t\t\t\tif (!strip(value).empty) {\n\t\t\t\t\t\tconfigFileSyncDir = value;\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file: \" ~ key);\n\t\t\t\t\t\taddLogEntry(\"ERROR: sync_dir in config file cannot be empty - this is a fatal error and must be corrected\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\tforceExit();\n\t\t\t\t\t}\n\t\t\t\t} else if (key == \"skip_file\") {\n\t\t\t\t\t// Flag this as true\n\t\t\t\t\tconfigFileSkipFileReadIn = true;\n\t\t\t\t\t// Merge safely, removing empty entries and de-duplicating\n\t\t\t\t\tconfigFileSkipFile = mergePipeDelimitedRulesDedup(configFileSkipFile, to!string(c.front.dup));\n\t\t\t\t\t// Update stored config value\n\t\t\t\t\tsetValueString(\"skip_file\", configFileSkipFile);\n\t\t\t\t} else if (key == \"skip_dir\") {\n\t\t\t\t\t// Merge safely, removing empty entries and de-duplicating\n\t\t\t\t\tconfigFileSkipDir = mergePipeDelimitedRulesDedup(configFileSkipDir, to!string(c.front.dup));\n\t\t\t\t\t// Update stored config value\n\t\t\t\t\tsetValueString(\"skip_dir\", configFileSkipDir);\n\t\t\t\t} else if (key == \"single_directory\") {\n\t\t\t\t\t// --single-directory Strip quotation marks from path \n\t\t\t\t\t// This is an issue when using ONEDRIVE_SINGLE_DIRECTORY with Docker\n\t\t\t\t\tstring configFileSingleDirectory = strip(to!string(c.front.dup), \"\\\"\");\n\t\t\t\t\tsetValueString(\"single_directory\", configFileSingleDirectory);\n\t\t\t\t} else if (key == \"azure_ad_endpoint\") {\n\t\t\t\t\tswitch (value) {\n\t\t\t\t\t\tcase \"\":\n\t\t\t\t\t\t\taddLogEntry(\"Using default config option for Global Azure AD Endpoints\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"USL4\":\n\t\t\t\t\t\t\taddLogEntry(\"Using config option for Azure AD for US Government Endpoints\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"USL5\":\n\t\t\t\t\t\t\taddLogEntry(\"Using config option for Azure AD for US Government Endpoints (DOD)\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"DE\":\n\t\t\t\t\t\t\taddLogEntry(\"Using config option for Azure AD Germany\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"CN\":\n\t\t\t\t\t\t\taddLogEntry(\"Using config option for Azure AD China operated by VNET\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\taddLogEntry(\"Unknown Azure AD Endpoint - using Global Azure AD Endpoints\");\n\t\t\t\t\t}\n\t\t\t\t} else if (key == \"transfer_order\") {\n\t\t\t\t\tswitch (value) {\n\t\t\t\t\t\tcase \"size_asc\":\n\t\t\t\t\t\t\taddLogEntry(\"Files will be transferred sorted by ascending size (smallest first)\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"size_dsc\":\n\t\t\t\t\t\t\taddLogEntry(\"Files will be transferred sorted by descending size (largest first)\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"name_asc\":\n\t\t\t\t\t\t\taddLogEntry(\"Files will be transferred sorted by ascending name (A -> Z)\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"name_dsc\":\n\t\t\t\t\t\t\taddLogEntry(\"Files will be transferred sorted by descending name (Z -> A)\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\taddLogEntry(\"Files will be transferred in original order that they were received (FIFO)\");\n\t\t\t\t\t}\n\t\t\t\t} else if (key == \"application_id\") {\n\t\t\t\t\tstring tempApplicationId = strip(value);\n\t\t\t\t\tif (tempApplicationId.empty) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using default value: \" ~ key);\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"application_id in config file cannot be empty - using default application_id\", [\"debug\"]);}\n\t\t\t\t\t\tsetValueString(\"application_id\", defaultApplicationId);\n\t\t\t\t\t}\n\t\t\t\t} else if (key == \"drive_id\") {\n\t\t\t\t\tstring tempDriveId = strip(value);\n\t\t\t\t\tif (tempDriveId.empty) {\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file: \" ~ key);\n\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\tforceExit();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconfigFileDriveId = tempDriveId;\n\t\t\t\t\t}\n\t\t\t\t} else if (key == \"log_dir\") {\n\t\t\t\t\tstring tempLogDir = strip(value);\n\t\t\t\t\tif (tempLogDir.empty) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using default value: \" ~ key);\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"log_dir in config file cannot be empty - using default log_dir\", [\"debug\"]);}\n\t\t\t\t\t\tsetValueString(\"log_dir\", defaultLogFileDir);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (key in longValues) {\n\t\t\t\tulong thisConfigValue;\n\t\t\t\ttry {\n\t\t\t\t\tthisConfigValue = to!ulong(c.front.dup);\n\t\t\t\t} catch (std.conv.ConvException) {\n\t\t\t\t\taddLogEntry(\"Invalid value for key in config file: \" ~ key);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tsetValueLong(key, thisConfigValue);\n\t\t\t\tif (key == \"monitor_interval\") { // if key is 'monitor_interval' the value must be 300 or greater\n\t\t\t\t\tulong tempValue = thisConfigValue;\n\t\t\t\t\t// the temp value needs to be 300 or greater\n\t\t\t\t\tif (tempValue < defaultMonitorInterval) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using default value: \" ~ key);\n\t\t\t\t\t\ttempValue = defaultMonitorInterval;\n\t\t\t\t\t}\n\t\t\t\t\tsetValueLong(\"monitor_interval\", tempValue);\n\t\t\t\t} else if (key == \"monitor_fullscan_frequency\") { // if key is 'monitor_fullscan_frequency' the value must be 12 or greater\n\t\t\t\t\tulong tempValue = thisConfigValue;\n\t\t\t\t\t// the temp value needs to be 12 or greater\n\t\t\t\t\tif (tempValue < 12) {\n\t\t\t\t\t\t// If this is not set to zero (0) then we are not disabling 'monitor_fullscan_frequency'\n\t\t\t\t\t\tif (tempValue != 0) {\n\t\t\t\t\t\t\t// invalid value\n\t\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using default value: \" ~ key);\n\t\t\t\t\t\t\ttempValue = 12;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tsetValueLong(\"monitor_fullscan_frequency\", tempValue);\n\t\t\t\t} else if (key == \"space_reservation\") { // if key is 'space_reservation' we have to calculate MB -> bytes\n\t\t\t\t\tulong tempValue = thisConfigValue;\n\t\t\t\t\t// a value of 0 needs to be made at least 1MB .. \n\t\t\t\t\tif (tempValue == 0) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using 1MB: \" ~ key);\n\t\t\t\t\t\ttempValue = 1;\n\t\t\t\t\t}\n\t\t\t\t\tsetValueLong(\"space_reservation\", tempValue * 2^^20);\n\t\t\t\t} else if (key == \"ip_protocol_version\") {\n\t\t\t\t\tulong tempValue = thisConfigValue;\n\t\t\t\t\tif (tempValue > 2) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using default value: \" ~ key);\n\t\t\t\t\t\ttempValue = defaultIpProtocol;\n\t\t\t\t\t}\n\t\t\t\t\tsetValueLong(\"ip_protocol_version\", tempValue);\n\t\t\t\t} else if (key == \"threads\") {\n\t\t\t\t\tulong tempValue = thisConfigValue;\n\t\t\t\t\tif (tempValue > 16) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using default value: \" ~ key);\n\t\t\t\t\t\ttempValue = defaultConcurrentThreads;\n\t\t\t\t\t}\n\t\t\t\t\tsetValueLong(\"threads\", tempValue);\n\t\t\t\t} else if (key == \"inotify_delay\") {\n\t\t\t\t\tulong tempValue = thisConfigValue;\n\t\t\t\t\tif ((tempValue < 5)||(tempValue > 15)) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using default value: \" ~ key);\n\t\t\t\t\t\ttempValue = defaultInotifyDelay;\n\t\t\t\t\t}\n\t\t\t\t\tsetValueLong(\"inotify_delay\", tempValue);\n\t\t\t\t} else if (key == \"skip_size\") {\n\t\t\t\t\t// Flag this for triggering --resync requirement\n\t\t\t\t\tconfigFileSkipSize = true;\n\t\t\t\t\tulong tempValue = thisConfigValue;\n\t\t\t\t\t// If set, this must be greater than 0\n\t\t\t\t\tif (tempValue <= 0) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file - using default value: \" ~ key);\n\t\t\t\t\t\ttempValue = 0;\n\t\t\t\t\t}\n\t\t\t\t\tsetValueLong(\"skip_size\", tempValue);\n\t\t\t\t} else if (key == \"file_fragment_size\") {\n\t\t\t\t\tulong tempValue = thisConfigValue;\n\t\t\t\t\t// If set, this must be greater than the default, but also aligning to Microsoft upper limit of 60 MiB\n\t\t\t\t\t// Enforce lower bound (must be greater than default)\n\t\t\t\t\tif (tempValue < defaultFileFragmentSize) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file (too low) - using default value: \" ~ key);\n\t\t\t\t\t\ttempValue = defaultFileFragmentSize;\n\t\t\t\t\t}\n\t\t\t\t\t// Enforce upper bound (safe maximum)\n\t\t\t\t\telse if (tempValue > defaultMaxFileFragmentSize) {\n\t\t\t\t\t\taddLogEntry(\"Invalid value for key in config file (too high) - using maximum safe value: \" ~ key);\n\t\t\t\t\t\ttempValue = defaultMaxFileFragmentSize;\n\t\t\t\t\t}\n\t\t\t\t\tsetValueLong(\"file_fragment_size\", tempValue);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"Unknown key in config file: \" ~ key);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// If we read in 'skip_file' from the 'config' file, this will be 'true'\n\t\tif (configFileSkipFileReadIn) {\n\t\t\t// The user added entries, are the application defaults included or were these discarded / discounted?\n\t\t\t// Check for temporary and/or transient files to skip (application defaults)\n\t\t\tcheckForSkipFileDefaults();\n\t\t}\n\t\t\n\t\t// Return that we were able to read in the config file and parse the options without issue\n\t\treturn true;\n\t}\n\t\n\t// Perform a check on 'skip_file' configuration post reading from 'config' file\n\tvoid checkForSkipFileDefaults() {\n\t\t// Split both the default and user values\n\t\tauto defaultEntries = defaultSkipFile.split('|').map!(a => a.strip).array;\n\t\tauto userEntries = configFileSkipFile.split('|').map!(a => a.strip).array;\n\n\t\tstring[] missingDefaults;\n\n\t\t// Check if all defaults exist in user config\n\t\tforeach (defaultEntry; defaultEntries) {\n\t\t\tif (!userEntries.canFind(defaultEntry)) {\n\t\t\t\tmissingDefaults ~= defaultEntry;\n\t\t\t}\n\t\t}\n\n\t\t// Display warning message about missing default entries for temporary and/or transient files that should be skipped\n\t\tif (!missingDefaults.empty) {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"WARNING: Your 'skip_file' configuration is missing important default entries. Temporary and/or transient files that would normally be skipped may now be included in syncing.\", [\"info\", \"notify\"]);\n\t\t\taddLogEntry();\n\t\t\tif (verboseLogging) {\n\t\t\t\taddLogEntry(\"By default, the following types of temporary and/or transient files are skipped:\", [\"verbose\"]);\n\t\t\t\taddLogEntry(\"  Files that start with '~' (Temporary or backup files that are not intended to be saved permanently)\", [\"verbose\"]);\n\t\t\t\taddLogEntry(\"  Files that start with '.~' (e.g., LibreOffice lock files)\", [\"verbose\"]);\n\t\t\t\taddLogEntry(\"  Files that end with '.tmp' (Generic temporary files created by applications like browsers, editors, installers)\", [\"verbose\"]);\n\t\t\t\taddLogEntry(\"  Files that end with '.swp' (Transient files created by editors such as vim and vi)\", [\"verbose\"]);\n\t\t\t\taddLogEntry(\"  Files that end with '.partial' (Partially downloaded files, incomplete by nature, should not be synced)\", [\"verbose\"]);\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"  Missing the following important 'skip_file' entries: \" ~ missingDefaults.join(\", \"), [\"verbose\"]);\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"Reference: https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md#skip_file\", [\"verbose\"]);\n\t\t\t\taddLogEntry();\n\t\t\t}\n\t\t}\n\t}\n\n\t// Update the application configuration based on CLI passed in parameters\n\tvoid updateFromArgs(string[] cliArgs) {\n\t\t// Add additional CLI options that are NOT configurable via config file\n\t\tstringValues[\"create_directory\"] = \"\";\n\t\tstringValues[\"create_share_link\"] = \"\";\n\t\tstringValues[\"destination_directory\"] = \"\";\n\t\tstringValues[\"get_file_link\"] = \"\";\n\t\tstringValues[\"modified_by\"] = \"\";\n\t\tstringValues[\"sharepoint_library_name\"] = \"\";\n\t\tstringValues[\"remove_directory\"] = \"\";\n\t\tstringValues[\"single_directory\"] = \"\";\n\t\tstringValues[\"source_directory\"] = \"\";\n\t\tstringValues[\"auth_files\"] = \"\";\n\t\tstringValues[\"auth_response\"] = \"\";\n\t\tstringValues[\"share_password\"] = \"\";\n\t\tstringValues[\"download_single_file\"] = \"\";\n\t\tboolValues[\"display_config\"] = false;\n\t\tboolValues[\"display_sync_status\"] = false;\n\t\tboolValues[\"display_quota\"] = false;\n\t\tboolValues[\"print_token\"] = false;\n\t\tboolValues[\"logout\"] = false;\n\t\tboolValues[\"reauth\"] = false;\n\t\tboolValues[\"monitor\"] = false;\n\t\tboolValues[\"synchronize\"] = false;\n\t\tboolValues[\"force\"] = false;\n\t\tboolValues[\"list_business_shared_items\"] = false;\n\t\tboolValues[\"sync_business_shared_files\"] = false;\n\t\tboolValues[\"force_sync\"] = false;\n\t\tboolValues[\"with_editing_perms\"] = false;\n\t\t\n\t\t// Specific options for CLI input handling\n\t\tstringValues[\"sync_dir_cli\"] = \"\";\n\t\t\n\t\t// Application Startup option validation\n\t\ttry {\n\t\t\tstring tmpStr;\n\t\t\tbool tmpBol;\n\t\t\tlong tmpVerb;\n\t\t\t// duplicated from main.d to get full help output!\n\t\t\tauto opt = getopt(\n\n\t\t\t\tcliArgs,\n\t\t\t\tstd.getopt.config.bundling,\n\t\t\t\tstd.getopt.config.caseSensitive,\n\t\t\t\t\"auth-files\",\n\t\t\t\t\t\"Perform authentication via files rather than an interactive dialogue. The application reads/writes the required values from/to the specified files\",\n\t\t\t\t\t&stringValues[\"auth_files\"],\n\t\t\t\t\"auth-response\",\n\t\t\t\t\t\"Perform authentication via a supplied response URL rather than an interactive dialogue\",\n\t\t\t\t\t&stringValues[\"auth_response\"],\n\t\t\t\t\"check-for-nomount\",\n\t\t\t\t\t\"Check for the presence of .nosync in the syncdir root. If found, do not perform sync\",\n\t\t\t\t\t&boolValues[\"check_nomount\"],\n\t\t\t\t\"check-for-nosync\",\n\t\t\t\t\t\"Check for the presence of .nosync in each directory. If found, skip directory from sync\",\n\t\t\t\t\t&boolValues[\"check_nosync\"],\n\t\t\t\t\"classify-as-big-delete\",\n\t\t\t\t\t\"Number of children in a path that is locally removed which will be classified as a 'big data delete'\",\n\t\t\t\t\t&longValues[\"classify_as_big_delete\"],\n\t\t\t\t\"cleanup-local-files\",\n\t\t\t\t\t\"Clean up additional local files when using --download-only. This will remove local data\",\n\t\t\t\t\t&boolValues[\"cleanup_local_files\"],\t\n\t\t\t\t\"create-directory\",\n\t\t\t\t\t\"Create a directory on OneDrive. No synchronisation will be performed\",\n\t\t\t\t\t&stringValues[\"create_directory\"],\n\t\t\t\t\"create-share-link\",\n\t\t\t\t\t\"Create a shareable link for an existing file on OneDrive\",\n\t\t\t\t\t&stringValues[\"create_share_link\"],\n\t\t\t\t\"debug-https\",\n\t\t\t\t\t\"Debug OneDrive HTTPS communication.\",\n\t\t\t\t\t&boolValues[\"debug_https\"],\n\t\t\t\t\"destination-directory\",\n\t\t\t\t\t\"Destination directory for renamed or moved items on OneDrive. No synchronisation will be performed\",\n\t\t\t\t\t&stringValues[\"destination_directory\"],\n\t\t\t\t\"disable-notifications\",\n\t\t\t\t\t\"Do not use desktop notifications in monitor mode\",\n\t\t\t\t\t&boolValues[\"disable_notifications\"],\n\t\t\t\t\"disable-download-validation\",\n\t\t\t\t\t\"Disable download validation when downloading from OneDrive\",\n\t\t\t\t\t&boolValues[\"disable_download_validation\"],\n\t\t\t\t\"disable-upload-validation\",\n\t\t\t\t\t\"Disable upload validation when uploading to OneDrive\",\n\t\t\t\t\t&boolValues[\"disable_upload_validation\"],\n\t\t\t\t\"display-config\",\n\t\t\t\t\t\"Display what options the client will use as currently configured. No synchronisation will be performed\",\n\t\t\t\t\t&boolValues[\"display_config\"],\n\t\t\t\t\"display-running-config\",\n\t\t\t\t\t\"Display what options the client has been configured to use on application startup\",\n\t\t\t\t\t&boolValues[\"display_running_config\"],\n\t\t\t\t\"display-sync-status\",\n\t\t\t\t\t\"Display the sync status of the client. No synchronisation will be performed\",\n\t\t\t\t\t&boolValues[\"display_sync_status\"],\n\t\t\t\t\"display-quota\",\n\t\t\t\t\t\"Display the quota status of the client. No synchronisation will be performed\",\n\t\t\t\t\t&boolValues[\"display_quota\"],\n\t\t\t\t\"download-only\",\n\t\t\t\t\t\"Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive\",\n\t\t\t\t\t&boolValues[\"download_only\"],\n\t\t\t\t\"download-file\",\n\t\t\t\t\t\"Download a single file from Microsoft OneDrive\",\n\t\t\t\t\t&stringValues[\"download_single_file\"],\n\t\t\t\t\"dry-run\",\n\t\t\t\t\t\"Perform a trial sync with no changes made\",\n\t\t\t\t\t&boolValues[\"dry_run\"],\n\t\t\t\t\"enable-logging\",\n\t\t\t\t\t\"Enable client activity to a separate log file\",\n\t\t\t\t\t&boolValues[\"enable_logging\"],\n\t\t\t\t\"file-fragment-size\",\n\t\t\t\t\t\"Specify the file fragment size for large file uploads (in MB)\",\n\t\t\t\t\t&longValues[\"file_fragment_size\"],\n\t\t\t\t\"force-http-11\",\n\t\t\t\t\t\"Force the use of HTTP 1.1 for all operations\",\n\t\t\t\t\t&boolValues[\"force_http_11\"],\n\t\t\t\t\"force\",\n\t\t\t\t\t\"Force the deletion of data when a 'big delete' is detected\",\n\t\t\t\t\t&boolValues[\"force\"],\n\t\t\t\t\"force-sync\",\n\t\t\t\t\t\"Force a synchronisation of a specific folder, only when using --sync --single-directory and ignore all non-default skip_dir and skip_file rules\",\n\t\t\t\t\t&boolValues[\"force_sync\"],\n\t\t\t\t\"get-file-link\",\n\t\t\t\t\t\"Display the file link of a synced file\",\n\t\t\t\t\t&stringValues[\"get_file_link\"],\n\t\t\t\t\"get-sharepoint-drive-id\",\n\t\t\t\t\t\"Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library\",\n\t\t\t\t\t&stringValues[\"sharepoint_library_name\"],\n\t\t\t\t\"get-O365-drive-id\",\n\t\t\t\t\t\"Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library (DEPRECATED)\",\n\t\t\t\t\t&stringValues[\"sharepoint_library_name\"],\n\t\t\t\t\"list-shared-items\",\n\t\t\t\t\t\"List OneDrive Business Shared Items\",\n\t\t\t\t\t&boolValues[\"list_business_shared_items\"],\n\t\t\t\t\"sync-shared-files\",\n\t\t\t\t\t\"Sync OneDrive Business Shared Files to the local filesystem\",\n\t\t\t\t\t&boolValues[\"sync_business_shared_files\"],\n\t\t\t\t\"local-first\",\n\t\t\t\t\t\"Synchronise from the local directory source first, before downloading changes from OneDrive\",\n\t\t\t\t\t&boolValues[\"local_first\"],\n\t\t\t\t\"log-dir\",\n\t\t\t\t\t\"Directory where logging output is saved to, needs to end with a slash\",\n\t\t\t\t\t&stringValues[\"log_dir\"],\n\t\t\t\t\"logout\",\n\t\t\t\t\t\"Log out the current user\",\n\t\t\t\t\t&boolValues[\"logout\"],\n\t\t\t\t\"modified-by\",\n\t\t\t\t\t\"Display the last modified by details of a given path\",\n\t\t\t\t\t&stringValues[\"modified_by\"],\n\t\t\t\t\"monitor|m\",\n\t\t\t\t\t\"Keep monitoring for local and remote changes\",\n\t\t\t\t\t&boolValues[\"monitor\"],\n\t\t\t\t\"monitor-interval\",\n\t\t\t\t\t\"Number of seconds by which each sync operation is undertaken when idle under monitor mode\",\n\t\t\t\t\t&longValues[\"monitor_interval\"],\n\t\t\t\t\"monitor-fullscan-frequency\",\n\t\t\t\t\t\"Number of sync runs before performing a full local scan of the synced directory\",\n\t\t\t\t\t&longValues[\"monitor_fullscan_frequency\"],\n\t\t\t\t\"monitor-log-frequency\",\n\t\t\t\t\t\"Frequency of logging in monitor mode\",\n\t\t\t\t\t&longValues[\"monitor_log_frequency\"],\n\t\t\t\t\"no-remote-delete\",\n\t\t\t\t\t\"Do not delete local file 'deletes' from OneDrive when using --upload-only\",\n\t\t\t\t\t&boolValues[\"no_remote_delete\"],\n\t\t\t\t\"print-access-token\",\n\t\t\t\t\t\"Print the access token, useful for debugging\",\n\t\t\t\t\t&boolValues[\"print_token\"],\n\t\t\t\t\"reauth\",\n\t\t\t\t\t\"Reauthenticate the client with OneDrive\",\n\t\t\t\t\t&boolValues[\"reauth\"],\n\t\t\t\t\"resync\",\n\t\t\t\t\t\"Forget the last saved state, perform a full sync\",\n\t\t\t\t\t&boolValues[\"resync\"],\n\t\t\t\t\"resync-auth\",\n\t\t\t\t\t\"Approve the use of performing a --resync action\",\n\t\t\t\t\t&boolValues[\"resync_auth\"],\n\t\t\t\t\"remove-directory\",\n\t\t\t\t\t\"Remove a directory on OneDrive. No synchronisation will be performed\",\n\t\t\t\t\t&stringValues[\"remove_directory\"],\n\t\t\t\t\"remove-source-files\",\n\t\t\t\t\t\"Remove source file after successful transfer to OneDrive when using --upload-only\",\n\t\t\t\t\t&boolValues[\"remove_source_files\"],\n\t\t\t\t\"remove-source-folders\",\n\t\t\t\t\t\"Remove the local directory structure post successful file transfer to Microsoft OneDrive when using --upload-only --remove-source-files\",\n\t\t\t\t\t&boolValues[\"remove_source_folders\"],\n\t\t\t\t\"single-directory\",\n\t\t\t\t\t\"Specify a single local directory within the OneDrive root to sync\",\n\t\t\t\t\t&stringValues[\"single_directory\"],\n\t\t\t\t\"skip-dot-files\",\n\t\t\t\t\t\"Skip dot files and folders from syncing\",\n\t\t\t\t\t&boolValues[\"skip_dotfiles\"],\n\t\t\t\t\"skip-file\",\n\t\t\t\t\t\"Skip any files that match this pattern from syncing\",\n\t\t\t\t\t&stringValues[\"skip_file\"],\n\t\t\t\t\"skip-dir\",\n\t\t\t\t\t\"Skip any directories that match this pattern from syncing\",\n\t\t\t\t\t&stringValues[\"skip_dir\"],\n\t\t\t\t\"skip-size\",\n\t\t\t\t\t\"Skip new files larger than this size (in MB)\",\n\t\t\t\t\t&longValues[\"skip_size\"],\n\t\t\t\t\"skip-dir-strict-match\",\n\t\t\t\t\t\"When matching skip_dir directories, only match explicit matches\",\n\t\t\t\t\t&boolValues[\"skip_dir_strict_match\"],\n\t\t\t\t\"skip-symlinks\",\n\t\t\t\t\t\"Skip syncing of symlinks\",\n\t\t\t\t\t&boolValues[\"skip_symlinks\"],\n\t\t\t\t\"source-directory\",\n\t\t\t\t\t\"Source directory to rename or move on OneDrive. No synchronisation will be performed\",\n\t\t\t\t\t&stringValues[\"source_directory\"],\n\t\t\t\t\"space-reservation\",\n\t\t\t\t\t\"The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation\",\n\t\t\t\t\t&longValues[\"space_reservation\"],\n\t\t\t\t\"syncdir\",\n\t\t\t\t\t\"Specify the local directory used for synchronisation to OneDrive\",\n\t\t\t\t\t&stringValues[\"sync_dir_cli\"],\n\t\t\t\t\"share-password\",\n\t\t\t\t\t\"Require a password to access the shared link when used with --create-share-link <file>\",\n\t\t\t\t\t&stringValues[\"share_password\"],\n\t\t\t\t\"sync|s\",\n\t\t\t\t\t\"Perform a synchronisation with Microsoft OneDrive\",\n\t\t\t\t\t&boolValues[\"synchronize\"],\n\t\t\t\t\"synchronize\",\n\t\t\t\t\t\"Perform a synchronisation with Microsoft OneDrive (DEPRECATED)\",\n\t\t\t\t\t&boolValues[\"synchronize\"],\n\t\t\t\t\"sync-root-files\",\n\t\t\t\t\t\"Sync all files in sync_dir root when using sync_list\",\n\t\t\t\t\t&boolValues[\"sync_root_files\"],\n\t\t\t\t\"threads\",\n\t\t\t\t\t\"Specify a value for the number of worker threads used for parallel upload and download operations\",\n\t\t\t\t\t&longValues[\"threads\"],\n\t\t\t\t\"upload-only\",\n\t\t\t\t\t\"Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive\",\n\t\t\t\t\t&boolValues[\"upload_only\"],\n\t\t\t\t\"confdir\",\n\t\t\t\t\t\"Set the directory used to store the configuration files\",\n\t\t\t\t\t&tmpStr,\n\t\t\t\t\"verbose|v+\",\n\t\t\t\t\t\"Print more details, useful for debugging (repeat for extra debugging)\",\n\t\t\t\t\t&tmpVerb,\n\t\t\t\t\"version\",\n\t\t\t\t\t\"Print the version and exit\",\n\t\t\t\t\t&tmpBol,\n\t\t\t\t\"with-editing-perms\",\n\t\t\t\t\t\"Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link <file>\",\n\t\t\t\t\t&boolValues[\"with_editing_perms\"]\n\t\t\t);\n\t\t\t\n\t\t\t// Was --syncdir specified?\n\t\t\tif (!getValueString(\"sync_dir_cli\").empty) {\n\t\t\t\t// Build the line we need to update and/or write out\n\t\t\t\tstring newConfigOptionSyncDirLine = \"sync_dir = \\\"\" ~ getValueString(\"sync_dir_cli\") ~ \"\\\"\";\n\t\t\t\t\n\t\t\t\t// Does a 'config' file exist?\n\t\t\t\tif (!exists(applicableConfigFilePath)) {\n\t\t\t\t\t// No existing 'config' file exists, create it, and write the 'sync_dir' configuration to it\n\t\t\t\t\tif (!getValueBool(\"dry_run\")) {\n\t\t\t\t\t\tstd.file.write(applicableConfigFilePath, newConfigOptionSyncDirLine);\n\t\t\t\t\t\t// Config file should only be readable by the user who created it - 0600 permissions needed\n\t\t\t\t\t\tapplicableConfigFilePath.setAttributes(convertedPermissionValue);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// an existing config file exists .. so this now becomes tricky\n\t\t\t\t\t// 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'\n\t\t\t\t\tif ( (getValueString(\"sync_dir\")) != (getValueString(\"sync_dir_cli\")) ) {\n\t\t\t\t\t\t// values are different\n\t\t\t\t\t\tFile applicableConfigFilePathFileHandle = File(applicableConfigFilePath, \"r\");\n\t\t\t\t\t\tstring lineBuffer;\n\t\t\t\t\t\tstring[] newConfigFileEntries;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// read applicableConfigFilePath line by line\n\t\t\t\t\t\tauto range = applicableConfigFilePathFileHandle.byLine();\n\t\t\t\t\t\t\n\t\t\t\t\t\t// for each 'config' file line\n\t\t\t\t\t\tforeach (line; range) {\n\t\t\t\t\t\t\tlineBuffer = stripLeft(line).to!string;\n\t\t\t\t\t\t\tif (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') {\n\t\t\t\t\t\t\t\tnewConfigFileEntries ~= [lineBuffer];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tauto c = lineBuffer.matchFirst(configRegex);\n\t\t\t\t\t\t\t\tif (!c.empty) {\n\t\t\t\t\t\t\t\t\tc.popFront(); // skip the whole match\n\t\t\t\t\t\t\t\t\tstring key = c.front.dup;\n\t\t\t\t\t\t\t\t\tif (key == \"sync_dir\") {\n\t\t\t\t\t\t\t\t\t\t// lineBuffer is the line we want to keep\n\t\t\t\t\t\t\t\t\t\tnewConfigFileEntries ~= [newConfigOptionSyncDirLine];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tnewConfigFileEntries ~= [lineBuffer];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// close original 'config' file if still open\n\t\t\t\t\t\tif (applicableConfigFilePathFileHandle.isOpen()) {\n\t\t\t\t\t\t\t// close open file\n\t\t\t\t\t\t\tapplicableConfigFilePathFileHandle.close();\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// free memory from file open\n\t\t\t\t\t\tobject.destroy(applicableConfigFilePathFileHandle);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Update the existing item in the file line array\n\t\t\t\t\t\tif (!getValueBool(\"dry_run\")) {\n\t\t\t\t\t\t\t// Open the file with write access using 'w' mode to overwrite existing content\n\t\t\t\t\t\t\tFile applicableConfigFilePathFileHandleWrite = File(applicableConfigFilePath, \"w\");\n\n\t\t\t\t\t\t\t// Write each line from the 'newConfigFileEntries' array to the file\n\t\t\t\t\t\t\tforeach (line; newConfigFileEntries) {\n\t\t\t\t\t\t\t\tapplicableConfigFilePathFileHandleWrite.writeln(line);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Is this a running as a container\n\t\t\t\t\t\t\tif (entrypointExists) {\n\t\t\t\t\t\t\t\t// write this to the config file so that when config options are checked again, this matches on next run\n\t\t\t\t\t\t\t\tapplicableConfigFilePathFileHandleWrite.writeln(newConfigOptionSyncDirLine);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Flush and close the file handle to ensure all data is written\n\t\t\t\t\t\t\tif (applicableConfigFilePathFileHandleWrite.isOpen()) {\n\t\t\t\t\t\t\tapplicableConfigFilePathFileHandleWrite.flush();\n\t\t\t\t\t\t\t\tapplicableConfigFilePathFileHandleWrite.close();\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// free memory from file open\n\t\t\t\t\t\t\tobject.destroy(applicableConfigFilePathFileHandleWrite);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// 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\n\t\t\t\tsetValueString(\"sync_dir\", getValueString(\"sync_dir_cli\"));\n\t\t\t}\n\t\t\t\n\t\t\t// Was --monitor-interval specified and now set to a value below minimum requirement?\n\t\t\tif (getValueLong(\"monitor_interval\") < defaultMonitorInterval ) {\n\t\t\t\taddLogEntry(\"Invalid value for --monitor-interval - using default value: \" ~ to!string(defaultMonitorInterval));\n\t\t\t\tsetValueLong(\"monitor_interval\", defaultMonitorInterval);\n\t\t\t}\n\t\t\t\n\t\t\t// Was --file-fragment-size specified and now set to a value below or above maximum?\n\t\t\t// Enforce lower bound (must be greater than default) for 'file_fragment_size'\n\t\t\tif (getValueLong(\"file_fragment_size\") < defaultFileFragmentSize) {\n\t\t\t\taddLogEntry(\"Invalid value for --file-fragment-size (too low) - using default value: \" ~ to!string(defaultFileFragmentSize));\n\t\t\t\tsetValueLong(\"file_fragment_size\", defaultFileFragmentSize);\n\t\t\t}\n\t\t\t// Enforce upper bound (safe maximum) for 'file_fragment_size'\n\t\t\tif (getValueLong(\"file_fragment_size\") > defaultMaxFileFragmentSize) {\n\t\t\t\taddLogEntry(\"Invalid value for --file-fragment-size (too high) - using maximum safe value: \" ~ to!string(defaultMaxFileFragmentSize));\n\t\t\t\tsetValueLong(\"file_fragment_size\", defaultMaxFileFragmentSize);\n\t\t\t}\n\t\t\t\n\t\t\t// Was --auth-files used?\n\t\t\tif (!getValueString(\"auth_files\").empty) {\n\t\t\t\t// --auth-files used, need to validate that '~' was not used as a path identifier, and if yes, perform the correct expansion\n\t\t\t\tstring[] tempAuthFiles = getValueString(\"auth_files\").split(\":\");\n\t\t\t\tstring tempAuthUrl = tempAuthFiles[0];\n\t\t\t\tstring tempResponseUrl = tempAuthFiles[1];\n\t\t\t\tstring newAuthFilesString;\n\t\t\t\t\n\t\t\t\t// shell expansion if required\n\t\t\t\tif (!shellEnvironmentSet){\n\t\t\t\t\t// No shell environment is set, no automatic expansion of '~' if present is possible\n\t\t\t\t\t// Does the 'currently configured' tempAuthUrl include a ~\n\t\t\t\t\tif (canFind(tempAuthUrl, \"~\")) {\t\n\t\t\t\t\t\t// A ~ was found in auth_files(authURL)\n\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\ttempAuthUrl = buildNormalizedPath(buildPath(defaultHomePath, strip(tempAuthUrl, \"~\")));\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Does the 'currently configured' tempAuthUrl include a ~\n\t\t\t\t\tif (canFind(tempResponseUrl, \"~\")) {\n\t\t\t\t\t\t// A ~ was found in auth_files(authURL)\n\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\ttempResponseUrl = buildNormalizedPath(buildPath(defaultHomePath, strip(tempResponseUrl, \"~\")));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Shell environment is set, automatic expansion of '~' if present is possible\n\t\t\t\t\t// Does the 'currently configured' tempAuthUrl include a ~\n\t\t\t\t\tif (canFind(tempAuthUrl, \"~\")) {\n\t\t\t\t\t\t// A ~ was found in auth_files(authURL)\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"auth_files: A '~' was found in the configured 'auth_files(authURL)', automatically expanding as SHELL and USER environment variable is set\", [\"debug\"]);}\n\t\t\t\t\t\ttempAuthUrl = expandTilde(tempAuthUrl);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Does the 'currently configured' tempAuthUrl include a ~\n\t\t\t\t\tif (canFind(tempResponseUrl, \"~\")) {\n\t\t\t\t\t\t// A ~ was found in auth_files(authURL)\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"auth_files: A '~' was found in the configured 'auth_files(tempResponseUrl)', automatically expanding as SHELL and USER environment variable is set\", [\"debug\"]);}\n\t\t\t\t\t\ttempResponseUrl = expandTilde(tempResponseUrl);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Build new string\n\t\t\t\tnewAuthFilesString = tempAuthUrl ~ \":\" ~ tempResponseUrl;\n\t\t\t\tif (debugLogging) {addLogEntry(\"auth_files - updated value: \" ~ newAuthFilesString, [\"debug\"]);}\n\t\t\t\tsetValueString(\"auth_files\", newAuthFilesString);\n\t\t\t}\n\t\t\t\n\t\t\tif (opt.helpWanted) {\n\t\t\t\toutputLongHelp(opt.options);\n\t\t\t\t// Shutdown logging, which also flushes all logging buffers\n\t\t\t\tshutdownLogging();\n\t\t\t\t// Exit as successful\n\t\t\t\texit(EXIT_SUCCESS);\n\t\t\t}\n\t\t} catch (GetOptException e) {\n\t\t\t// getOpt error - must use writeln() here\n\t\t\twriteln(e.msg);\n\t\t\twriteln(\"Try 'onedrive -h' for more information\");\n\t\t\t// Shutdown logging, which also flushes all logging buffers\n\t\t\tshutdownLogging();\n\t\t\t// Exit as failure\n\t\t\texit(EXIT_FAILURE);\n\t\t} catch (Exception e) {\n\t\t\t// general error - must use writeln() here\n\t\t\twriteln(e.msg);\n\t\t\twriteln(\"Try 'onedrive -h' for more information\");\n\t\t\t// Shutdown logging, which also flushes all logging buffers\n\t\t\tshutdownLogging();\n\t\t\t// Exit as failure\n\t\t\texit(EXIT_FAILURE);\n\t\t}\n\t}\n\t\n\t// Check the arguments passed in for any that will be deprecated\n\tvoid checkDeprecatedOptions(string[] cliArgs) {\n\t\n\t\tbool deprecatedCommandsFound = false;\n\t\n\t\tforeach (cliArg; cliArgs) {\n\t\t\t// Check each CLI arg for items that have been deprecated\n\t\t\t\n\t\t\t// --synchronize deprecated in v2.5.0, will be removed in future version\n\t\t\tif (cliArg == \"--synchronize\") {\n\t\t\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\t\t\taddLogEntry(\"DEPRECIATION WARNING: --synchronize has been deprecated in favour of --sync or -s\");\n\t\t\t\tdeprecatedCommandsFound = true;\n\t\t\t}\n\t\t\t\n\t\t\t// --get-O365-drive-id deprecated in v2.5.0, will be removed in future version\n\t\t\tif (cliArg == \"--get-O365-drive-id\") {\n\t\t\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\t\t\taddLogEntry(\"DEPRECIATION WARNING: --get-O365-drive-id has been deprecated in favour of --get-sharepoint-drive-id\");\n\t\t\t\tdeprecatedCommandsFound = true;\n\t\t\t}\n\t\t}\n\t\n\t\tif (deprecatedCommandsFound) {\n\t\t\taddLogEntry(\"DEPRECIATION WARNING: Deprecated commands will be removed in a future release.\");\n\t\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\t}\n\t}\n\t\n\t// Display the applicable application configuration\n\tvoid displayApplicationConfiguration() {\n\t\tif (getValueBool(\"display_running_config\")) {\n\t\t\taddLogEntry(\"--------------- Application Runtime Configuration ---------------\");\n\t\t}\n\t\t\n\t\t// Display application version\n\t\taddLogEntry(\"Application version                          = \" ~ applicationVersion);\n\t\taddLogEntry(\"Compiled with                                = \" ~ compilerDetails());\n\t\taddLogEntry(\"Curl version                                 = \" ~ getCurlVersionString());\n\t\t\n\t\t// Display all of the pertinent configuration options\n\t\taddLogEntry(\"User Application Config path                 = \" ~ configDirName);\n\t\taddLogEntry(\"System Application Config path               = \" ~ systemConfigDirName);\n\t\t\n\t\t// Does a config file exist or are we using application defaults\n\t\taddLogEntry(\"Applicable Application 'config' location     = \" ~ applicableConfigFilePath);\n\t\t\n\t\tstring configFileStatusMessage;\n\t\tif (exists(applicableConfigFilePath)) {\n\t\t\tconfigFileStatusMessage = \"true - using 'config' file values to override application defaults\";\n\t\t} else {\n\t\t\tconfigFileStatusMessage = \"false - using application defaults\";\n\t\t}\n\t\taddLogEntry(\"Configuration file found in config location  = \" ~ configFileStatusMessage);\n\t\t\n\t\t// Display where various files should live\n\t\t// - items.sqlite3\n\t\t// - sync_list\n\t\t// If using the 'system' directory, (/etc/onedrive) for the config file, these should always live in the 'users' home directory\n\t\taddLogEntry(\"Applicable 'sync_list' location              = \" ~ syncListFilePath);\n\t\taddLogEntry(\"Applicable 'items.sqlite3' location          = \" ~ databaseFilePath);\n\t\t\n\t\t// Is config option drive_id configured?\n\t\taddLogEntry(\"Config option 'drive_id'                     = \" ~ getValueString(\"drive_id\"));\n\t\t\n\t\t// Config Options as per 'config' file\n\t\taddLogEntry(\"Config option 'sync_dir'                     = \" ~ getValueString(\"sync_dir\"));\n\t\t\n\t\t// authentication\n\t\taddLogEntry(\"Config option 'use_intune_sso'               = \" ~ to!string(getValueBool(\"use_intune_sso\")));\n\t\taddLogEntry(\"Config option 'use_device_auth'              = \" ~ to!string(getValueBool(\"use_device_auth\")));\n\t\t\t\t\n\t\t// logging and notifications\n\t\taddLogEntry(\"Config option 'enable_logging'               = \" ~ to!string(getValueBool(\"enable_logging\")));\n\t\taddLogEntry(\"Config option 'log_dir'                      = \" ~ getValueString(\"log_dir\"));\n\t\taddLogEntry(\"Config option 'disable_notifications'        = \" ~ to!string(getValueBool(\"disable_notifications\")));\n\t\t\n\t\t// skip files and directory and 'matching' policy\n\t\taddLogEntry(\"Config option 'skip_dir'                     = \" ~ getValueString(\"skip_dir\"));\n\t\taddLogEntry(\"Config option 'skip_dir_strict_match'        = \" ~ to!string(getValueBool(\"skip_dir_strict_match\")));\n\t\taddLogEntry(\"Config option 'skip_file'                    = \" ~ getValueString(\"skip_file\"));\n\t\taddLogEntry(\"Config option 'skip_dotfiles'                = \" ~ to!string(getValueBool(\"skip_dotfiles\")));\n\t\taddLogEntry(\"Config option 'skip_symlinks'                = \" ~ to!string(getValueBool(\"skip_symlinks\")));\n\t\taddLogEntry(\"Config option 'skip_size'                    = \" ~ to!string(getValueLong(\"skip_size\")));\n\t\t\n\t\t// --monitor sync process options\n\t\taddLogEntry(\"Config option 'monitor_interval'             = \" ~ to!string(getValueLong(\"monitor_interval\")));\n\t\taddLogEntry(\"Config option 'monitor_log_frequency'        = \" ~ to!string(getValueLong(\"monitor_log_frequency\")));\n\t\taddLogEntry(\"Config option 'monitor_fullscan_frequency'   = \" ~ to!string(getValueLong(\"monitor_fullscan_frequency\")));\n\t\taddLogEntry(\"Config option 'disable_websocket_support'    = \" ~ to!string(getValueBool(\"disable_websocket_support\")));\n\t\t\n\t\t// sync process and method\n\t\taddLogEntry(\"Config option 'read_only_auth_scope'         = \" ~ to!string(getValueBool(\"read_only_auth_scope\")));\n\t\taddLogEntry(\"Config option 'dry_run'                      = \" ~ to!string(getValueBool(\"dry_run\")));\n\t\taddLogEntry(\"Config option 'upload_only'                  = \" ~ to!string(getValueBool(\"upload_only\")));\n\t\taddLogEntry(\"Config option 'download_only'                = \" ~ to!string(getValueBool(\"download_only\")));\n\t\taddLogEntry(\"Config option 'local_first'                  = \" ~ to!string(getValueBool(\"local_first\")));\n\t\taddLogEntry(\"Config option 'check_nosync'                 = \" ~ to!string(getValueBool(\"check_nosync\")));\n\t\taddLogEntry(\"Config option 'check_nomount'                = \" ~ to!string(getValueBool(\"check_nomount\")));\n\t\taddLogEntry(\"Config option 'resync'                       = \" ~ to!string(getValueBool(\"resync\")));\n\t\taddLogEntry(\"Config option 'resync_auth'                  = \" ~ to!string(getValueBool(\"resync_auth\")));\n\t\taddLogEntry(\"Config option 'cleanup_local_files'          = \" ~ to!string(getValueBool(\"cleanup_local_files\")));\n\t\taddLogEntry(\"Config option 'disable_permission_set'       = \" ~ to!string(getValueBool(\"disable_permission_set\")));\n\t\taddLogEntry(\"Config option 'transfer_order'               = \" ~ getValueString(\"transfer_order\"));\n\t\taddLogEntry(\"Config option 'delay_inotify_processing'     = \" ~ to!string(getValueBool(\"delay_inotify_processing\")));\n\t\taddLogEntry(\"Config option 'inotify_delay'                = \" ~ to!string(getValueLong(\"inotify_delay\")));\n\t\taddLogEntry(\"Config option 'display_transfer_metrics'     = \" ~ to!string(getValueBool(\"display_transfer_metrics\")));\n\t\taddLogEntry(\"Config option 'force_session_upload'         = \" ~ to!string(getValueBool(\"force_session_upload\")));\n\t\taddLogEntry(\"Config option 'file_fragment_size'           = \" ~ to!string(getValueLong(\"file_fragment_size\")));\n\t\t\n\t\t// data integrity\n\t\taddLogEntry(\"Config option 'classify_as_big_delete'       = \" ~ to!string(getValueLong(\"classify_as_big_delete\")));\n\t\taddLogEntry(\"Config option 'disable_upload_validation'    = \" ~ to!string(getValueBool(\"disable_upload_validation\")));\n\t\taddLogEntry(\"Config option 'disable_download_validation'  = \" ~ to!string(getValueBool(\"disable_download_validation\")));\n\t\taddLogEntry(\"Config option 'bypass_data_preservation'     = \" ~ to!string(getValueBool(\"bypass_data_preservation\")));\n\t\taddLogEntry(\"Config option 'no_remote_delete'             = \" ~ to!string(getValueBool(\"no_remote_delete\")));\n\t\taddLogEntry(\"Config option 'remove_source_files'          = \" ~ to!string(getValueBool(\"remove_source_files\")));\n\t\taddLogEntry(\"Config option 'sync_dir_permissions'         = \" ~ to!string(getValueLong(\"sync_dir_permissions\")));\n\t\taddLogEntry(\"Config option 'sync_file_permissions'        = \" ~ to!string(getValueLong(\"sync_file_permissions\")));\n\t\taddLogEntry(\"Config option 'space_reservation'            = \" ~ to!string(getValueLong(\"space_reservation\")));\n\t\taddLogEntry(\"Config option 'permanent_delete'             = \" ~ to!string(getValueBool(\"permanent_delete\")));\n\t\taddLogEntry(\"Config option 'write_xattr_data'             = \" ~ to!string(getValueBool(\"write_xattr_data\")));\n\t\taddLogEntry(\"Config option 'create_new_file_version'      = \" ~ to!string(getValueBool(\"create_new_file_version\")));\n\t\t\n\t\t// curl operations\n\t\taddLogEntry(\"Config option 'application_id'               = \" ~ getValueString(\"application_id\"));\n\t\taddLogEntry(\"Config option 'azure_ad_endpoint'            = \" ~ getValueString(\"azure_ad_endpoint\"));\n\t\taddLogEntry(\"Config option 'azure_tenant_id'              = \" ~ getValueString(\"azure_tenant_id\"));\n\t\taddLogEntry(\"Config option 'user_agent'                   = \" ~ getValueString(\"user_agent\"));\n\t\taddLogEntry(\"Config option 'force_http_11'                = \" ~ to!string(getValueBool(\"force_http_11\")));\n\t\taddLogEntry(\"Config option 'debug_https'                  = \" ~ to!string(getValueBool(\"debug_https\")));\n\t\taddLogEntry(\"Config option 'rate_limit'                   = \" ~ to!string(getValueLong(\"rate_limit\")));\n\t\taddLogEntry(\"Config option 'operation_timeout'            = \" ~ to!string(getValueLong(\"operation_timeout\")));\n\t\taddLogEntry(\"Config option 'dns_timeout'                  = \" ~ to!string(getValueLong(\"dns_timeout\")));\n\t\taddLogEntry(\"Config option 'connect_timeout'              = \" ~ to!string(getValueLong(\"connect_timeout\")));\n\t\taddLogEntry(\"Config option 'data_timeout'                 = \" ~ to!string(getValueLong(\"data_timeout\")));\n\t\taddLogEntry(\"Config option 'ip_protocol_version'          = \" ~ to!string(getValueLong(\"ip_protocol_version\")));\n\t\taddLogEntry(\"Config option 'threads'                      = \" ~ to!string(getValueLong(\"threads\")));\n\t\taddLogEntry(\"Config option 'max_curl_idle'                = \" ~ to!string(getValueLong(\"max_curl_idle\")));\n\t\t\n\t\t// GUI notifications\n\t\tversion(Notifications) {\n\t\t\taddLogEntry(\"Environment var 'XDG_RUNTIME_DIR'            = \" ~ to!string(xdg_exists));\n\t\t\taddLogEntry(\"Environment var 'DBUS_SESSION_BUS_ADDRESS'   = \" ~ to!string(dbus_exists));\n\t\t\taddLogEntry(\"Config option 'notify_file_actions'          = \" ~ to!string(getValueBool(\"notify_file_actions\")));\n\t\t} else {\n\t\t\taddLogEntry(\"Compile time option --enable-notifications   = false\");\n\t\t}\n\t\t\n\t\t// Recycle Bin \n\t\taddLogEntry(\"Config option 'use_recycle_bin'              = \" ~ to!string(getValueBool(\"use_recycle_bin\")));\n\t\taddLogEntry(\"Config option 'recycle_bin_path'             = \" ~ getValueString(\"recycle_bin_path\"));\n\t\t\t\t\n\t\t// Is sync_list configured and contains entries?\n\t\tif (exists(syncListFilePath) && getSize(syncListFilePath) > 0) {\n\t\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\t\taddLogEntry(\"Selective sync 'sync_list' configured        = true\");\n\t\t\taddLogEntry(\"sync_list config option 'sync_root_files'    = \" ~ to!string(getValueBool(\"sync_root_files\")));\n\t\t\taddLogEntry(\"sync_list contents:\");\n\t\t\t// Output the sync_list contents\n\t\t\tauto syncListFile = File(syncListFilePath, \"r\");\n\t\t\tauto range = syncListFile.byLine();\n\t\t\taddLogEntry(\"------------------------------'sync_list'------------------------------\");\n\t\t\tforeach (line; range) {\n\t\t\t\taddLogEntry(to!string(line));\n\t\t\t}\n\t\t\taddLogEntry(\"-----------------------------------------------------------------------\");\n\t\t\t\n\t\t\t// Close reading the 'sync_list' file\n\t\t\tsyncListFile.close();\n\t\t} else {\n\t\t\t// file does not exist or file size is not greater than 0\n\t\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\t\tif (exists(syncListFilePath) && getSize(syncListFilePath) == 0) {\n\t\t\t\t// 'sync_list' file exists, no entries\n\t\t\t\taddLogEntry(\"Selective sync 'sync_list' configured        = file exists but contains zero data\");\n\t\t\t} else {\n\t\t\t\t// no 'sync_list' file\n\t\t\t\taddLogEntry(\"Selective sync 'sync_list' configured        = false\");\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Is sync_business_shared_items enabled and configured ?\n\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\taddLogEntry(\"Config option 'sync_business_shared_items'   = \" ~ to!string(getValueBool(\"sync_business_shared_items\")));\n\t\tif (getValueBool(\"sync_business_shared_items\")) {\n\t\t\t// display what the shared files directory will be\n\t\t\taddLogEntry(\"Config option 'Shared Files Directory'       = \" ~ configuredBusinessSharedFilesDirectoryName);\n\t\t}\n\t\t\n\t\t// Are webhooks enabled?\n\t\taddLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering\n\t\taddLogEntry(\"Config option 'webhook_enabled'              = \" ~ to!string(getValueBool(\"webhook_enabled\")));\n\t\tif (getValueBool(\"webhook_enabled\")) {\n\t\t\taddLogEntry(\"Config option 'webhook_public_url'           = \" ~ getValueString(\"webhook_public_url\"));\n\t\t\taddLogEntry(\"Config option 'webhook_listening_host'       = \" ~ getValueString(\"webhook_listening_host\"));\n\t\t\taddLogEntry(\"Config option 'webhook_listening_port'       = \" ~ to!string(getValueLong(\"webhook_listening_port\")));\n\t\t\taddLogEntry(\"Config option 'webhook_expiration_interval'  = \" ~ to!string(getValueLong(\"webhook_expiration_interval\")));\n\t\t\taddLogEntry(\"Config option 'webhook_renewal_interval'     = \" ~ to!string(getValueLong(\"webhook_renewal_interval\")));\n\t\t\taddLogEntry(\"Config option 'webhook_retry_interval'       = \" ~ to!string(getValueLong(\"webhook_retry_interval\")));\n\t\t}\n\t\t\n\t\tif (getValueBool(\"display_running_config\")) {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"--------------------DEVELOPER_OPTIONS----------------------------\");\n\t\t\taddLogEntry(\"Config option 'force_children_scan'          = \" ~ to!string(getValueBool(\"force_children_scan\")));\n\t\t\taddLogEntry(\"Config option 'monitor_max_loop'             = \" ~ to!string(getValueLong(\"monitor_max_loop\")));\n\t\t\taddLogEntry(\"Config option 'display_memory'               = \" ~ to!string(getValueBool(\"display_memory\")));\n\t\t\taddLogEntry(\"Config option 'display_sync_options'         = \" ~ to!string(getValueBool(\"display_sync_options\")));\n\t\t\taddLogEntry(\"Config option 'display_processing_time'      = \" ~ to!string(getValueBool(\"display_processing_time\")));\n\t\t}\n\t\t\n\t\t// Close out config output\n\t\tif (getValueBool(\"display_running_config\")) {\n\t\t\taddLogEntry(\"-----------------------------------------------------------------\");\n\t\t\taddLogEntry();\n\t\t}\n\t}\n\t\n\t// Prompt the user to accept the risk of using --resync\n\tbool displayResyncRiskForAcceptance() {\n\t\t// what is the user risk acceptance?\n\t\tbool userRiskAcceptance = false;\n\t\t\n\t\t// Did the user use --resync-auth or 'resync_auth' in the config file to negate presenting this message?\n\t\tif (!getValueBool(\"resync_auth\")) {\n\t\t\t// need to prompt user\n\t\t\tchar response;\n\t\t\t\n\t\t\t// --resync warning message\n\t\t\taddLogEntry(\"\", [\"consoleOnly\"]); // new line, console only\n\t\t\taddLogEntry(\"WARNING: You have asked the client to perform a --resync operation.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         This operation will delete the client’s local state database and rebuild it entirely from the current online OneDrive state.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         Because the previous sync state will no longer be available, the following may occur:\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         * Local files that also exist in OneDrive may have local changes overwritten by the cloud version if a conflict cannot be safely resolved.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         * Local files may be renamed or duplicated locally as part of conflict resolution and data-preservation handling.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         * The initial synchronisation pass may involve a large number of file uploads and downloads.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         * The increased activity against the Microsoft Graph API may trigger HTTP 429 (throttling) responses during the synchronisation process.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         For safest operation:\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         * Ensure you have a current backup of your sync_dir.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         * Run this command first with --dry-run to confirm all planned actions.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"         * Enable 'use_recycle_bin' so that online deletion events from OneDrive are moved to your system Trash rather than deleted from your local disk.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"If in doubt, stop now and back up your local data before continuing.\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"\", [\"consoleOnly\"]);\n\t\t\taddLogEntry(\"Are you sure you wish to proceed with --resync? [Y/N] \", [\"consoleOnlyNoNewLine\"]);\n\t\t\t\t\t\t\n\t\t\ttry {\n\t\t\t\t// Attempt to read user response\n\t\t\t\tstring input = readln().strip;\n\t\t\t\tif (input.length > 0) {\n\t\t\t\t\tresponse = std.ascii.toUpper(input[0]);\n\t\t\t\t}\n\t\t\t} catch (std.format.FormatException e) {\n\t\t\t\tuserRiskAcceptance = false;\n\t\t\t\t// Caught an error\n\t\t\t\treturn EXIT_FAILURE;\n\t\t\t}\n\t\t\t\n\t\t\t// What did the user enter?\n\t\t\tif (debugLogging) {addLogEntry(\"--resync warning User Response Entered: \" ~ to!string(response), [\"debug\"]);}\n\t\t\t\n\t\t\t// Evaluate user response\n\t\t\tif ((to!string(response) == \"y\") || (to!string(response) == \"Y\")) {\n\t\t\t\t// User has accepted --resync risk to proceed\n\t\t\t\tuserRiskAcceptance = true;\n\t\t\t\t// Are you sure you wish .. does not use writeln();\n\t\t\t\twrite(\"\\n\");\n\t\t\t}\n\t\t} else {\n\t\t\t// resync_auth is true\n\t\t\tuserRiskAcceptance = true;\n\t\t}\n\t\t\n\t\t// Return the --resync acceptance or not\n\t\treturn userRiskAcceptance;\n\t}\n\t\n\t// Prompt the user to accept the risk of using --force-sync\n\tbool displayForceSyncRiskForAcceptance() {\n\t\t// what is the user risk acceptance?\n\t\tbool userRiskAcceptance = false;\n\t\t\n\t\t// need to prompt user\n\t\tchar response;\n\t\t\n\t\t// --force-sync warning message\n\t\taddLogEntry(\"\", [\"consoleOnly\"]); // new line, console only\n\t\taddLogEntry(\"The use of --force-sync will reconfigure the application to use defaults. This may have untold and unknown future impacts.\", [\"consoleOnly\"]);\n\t\taddLogEntry(\"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\"]);\n\t\taddLogEntry(\"\", [\"consoleOnly\"]); // new line, console only\n\t\taddLogEntry(\"Are you sure you wish to proceed with --force-sync [Y/N] \", [\"consoleOnlyNoNewLine\"]);\n\t\t\t\t\n\t\ttry {\n\t\t\t// Attempt to read user response\n\t\t\tstring input = readln().strip;\n\t\t\tif (input.length > 0) {\n\t\t\t\tresponse = std.ascii.toUpper(input[0]);\n\t\t\t}\n\t\t} catch (std.format.FormatException e) {\n\t\t\tuserRiskAcceptance = false;\n\t\t\t// Caught an error\n\t\t\treturn EXIT_FAILURE;\n\t\t}\n\t\t\n\t\t// What did the user enter?\n\t\tif (debugLogging) {addLogEntry(\"--force-sync warning User Response Entered: \" ~ to!string(response), [\"debug\"]);}\n\t\t\n\t\t// Evaluate user response\n\t\tif ((to!string(response) == \"y\") || (to!string(response) == \"Y\")) {\n\t\t\t// User has accepted --force-sync risk to proceed\n\t\t\tuserRiskAcceptance = true;\n\t\t\t// Are you sure you wish .. does not use writeln();\n\t\t\twrite(\"\\n\");\n\t\t}\n\t\t\n\t\t// Return the --resync acceptance or not\n\t\treturn userRiskAcceptance;\n\t}\n\t\n\t// Check the application configuration for any changes that need to trigger a --resync\n\t// This function is only called if --resync is not present\n\tbool applicationChangeWhereResyncRequired() {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\t// Default is that no resync is required\n\t\tbool resyncRequired = false;\n\n\t\t// Consolidate the flags for different configuration changes\n\t\tbool[11] configOptionsDifferent;\n\n\t\t// Handle multiple entries of skip_file\n\t\tstring backupConfigFileSkipFile;\n\t\t\n\t\t// Handle multiple entries of skip_dir\n\t\tstring backupConfigFileSkipDir;\n\t\t\n\t\t// Create and read the required initial hash files\n\t\tcreateRequiredInitialConfigurationHashFiles();\n\t\t\n\t\t// Read in the existing hash file values\n\t\treadExistingConfigurationHashFiles();\n\t\t\n\t\t// can we read the backup config file\n\t\tbool failedToReadBackupConfig = false;\n\n\t\t// Helper lambda for logging and setting the difference flag\n\t\tauto logAndSetDifference = (string message, size_t index) {\n\t\t\tif (debugLogging) {addLogEntry(message, [\"debug\"]);}\n\t\t\tconfigOptionsDifferent[index] = true;\n\t\t};\n\n\t\t// Check for changes in the sync_list and business_shared_items files\n\t\tif (currentSyncListHash != previousSyncListHash)\n\t\t\tlogAndSetDifference(\"sync_list file has been updated, --resync needed\", 0);\n\n\t\t// Check for updates in the config file\n\t\tif (currentConfigHash != previousConfigHash) {\n\t\t\taddLogEntry(\"Application configuration file has been updated, checking if --resync needed\");\n\t\t\tif (debugLogging) {addLogEntry(\"Using this configBackupFile: \" ~ configBackupFile, [\"debug\"]);}\n\n\t\t\tif (exists(configBackupFile)) {\n\t\t\t\tstring[string] backupConfigStringValues;\n\t\t\t\tbackupConfigStringValues[\"check_nosync\"] = \"\";\n\t\t\t\tbackupConfigStringValues[\"drive_id\"] = \"\";\n\t\t\t\tbackupConfigStringValues[\"sync_dir\"] = \"\";\n\t\t\t\tbackupConfigStringValues[\"skip_file\"] = \"\";\n\t\t\t\tbackupConfigStringValues[\"skip_dir\"] = \"\";\n\t\t\t\tbackupConfigStringValues[\"skip_dotfiles\"] = \"\";\n\t\t\t\tbackupConfigStringValues[\"skip_size\"] = \"\";\n\t\t\t\tbackupConfigStringValues[\"skip_symlinks\"] = \"\";\n\t\t\t\tbackupConfigStringValues[\"sync_business_shared_items\"] = \"\";\n\n\t\t\t\tbool check_nosync_present = false;\n\t\t\t\tbool drive_id_present = false;\n\t\t\t\tbool sync_dir_present = false;\n\t\t\t\tbool skip_file_present = false;\n\t\t\t\tbool skip_dir_present = false;\n\t\t\t\tbool skip_dotfiles_present = false;\n\t\t\t\tbool skip_size_present = false;\n\t\t\t\tbool skip_symlinks_present = false;\n\t\t\t\tbool sync_business_shared_items_present = false;\n\n\t\t\t\tstring configOptionModifiedMessage = \" was modified since the last time the application was successfully run, --resync required\";\n\t\t\t\tFile configBackupFileHandle;\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\tconfigBackupFileHandle = File(configBackupFile, \"r\");\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// filesystem error\n\t\t\t\t\tfailedToReadBackupConfig = true;\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, configBackupFile, FsErrorSeverity.warning);\n\t\t\t\t} catch (std.exception.ErrnoException e) {\n\t\t\t\t\t// filesystem error\n\t\t\t\t\tfailedToReadBackupConfig = true;\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, configBackupFile, FsErrorSeverity.warning);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tscope(exit) {\n\t\t\t\t\tif (configBackupFileHandle.isOpen()) {\n\t\t\t\t\t\tconfigBackupFileHandle.close();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (!failedToReadBackupConfig) {\n\t\t\t\t\t// backup config file was able to be read\n\t\t\t\t\tstring lineBuffer;\n\t\t\t\t\tauto range = configBackupFileHandle.byLine();\n\t\t\t\t\tforeach (line; range) {\n\t\t\t\t\t\tlineBuffer = stripLeft(line).to!string;\n\t\t\t\t\t\tif (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue;\n\t\t\t\t\t\tauto c = lineBuffer.matchFirst(configRegex);\n\t\t\t\t\t\tif (!c.empty) {\n\t\t\t\t\t\t\tc.popFront(); // skip the whole match\n\t\t\t\t\t\t\tstring key = c.front.dup;\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Backup Config Key: \" ~ key, [\"debug\"]);}\n\n\t\t\t\t\t\t\tauto p = key in backupConfigStringValues;\n\t\t\t\t\t\t\tif (p) {\n\t\t\t\t\t\t\t\tc.popFront();\n\t\t\t\t\t\t\t\tstring value = c.front.dup;\n\t\t\t\t\t\t\t\t// Compare each key value with current config\n\t\t\t\t\t\t\t\tif (key == \"drive_id\") {\n\t\t\t\t\t\t\t\t\tdrive_id_present = true;\n\t\t\t\t\t\t\t\t\tif (value != getValueString(\"drive_id\")) {\n\t\t\t\t\t\t\t\t\t\tlogAndSetDifference(key ~ configOptionModifiedMessage, 2);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (key == \"sync_dir\") {\n\t\t\t\t\t\t\t\t\tsync_dir_present = true;\n\t\t\t\t\t\t\t\t\tif (value != getValueString(\"sync_dir\")) {\n\t\t\t\t\t\t\t\t\t\tlogAndSetDifference(key ~ configOptionModifiedMessage, 3);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// skip_file handling\n\t\t\t\t\t\t\t\tif (key == \"skip_file\") {\n\t\t\t\t\t\t\t\t\tskip_file_present = true;\n\t\t\t\t\t\t\t\t\t// Merge safely, removing empty entries and de-duplicating\n\t\t\t\t\t\t\t\t\tbackupConfigFileSkipFile = mergePipeDelimitedRulesDedup(backupConfigFileSkipFile, to!string(c.front.dup));\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// skip_dir handling\n\t\t\t\t\t\t\t\tif (key == \"skip_dir\") {\n\t\t\t\t\t\t\t\t\tskip_dir_present = true;\n\t\t\t\t\t\t\t\t\t// Merge safely, removing empty entries and de-duplicating\n\t\t\t\t\t\t\t\t\tbackupConfigFileSkipDir = mergePipeDelimitedRulesDedup(backupConfigFileSkipDir, to!string(c.front.dup));\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif (key == \"skip_dotfiles\") {\n\t\t\t\t\t\t\t\t\tskip_dotfiles_present = true;\n\t\t\t\t\t\t\t\t\tif (value != to!string(getValueBool(\"skip_dotfiles\"))) {\n\t\t\t\t\t\t\t\t\t\tlogAndSetDifference(key ~ configOptionModifiedMessage, 6);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (key == \"skip_symlinks\") {\n\t\t\t\t\t\t\t\t\tskip_symlinks_present = true;\n\t\t\t\t\t\t\t\t\tif (value != to!string(getValueBool(\"skip_symlinks\"))) {\n\t\t\t\t\t\t\t\t\t\tlogAndSetDifference(key ~ configOptionModifiedMessage, 7);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (key == \"sync_business_shared_items\") {\n\t\t\t\t\t\t\t\t\tsync_business_shared_items_present = true;\n\t\t\t\t\t\t\t\t\tif (value != to!string(getValueBool(\"sync_business_shared_items\"))) {\n\t\t\t\t\t\t\t\t\t\tlogAndSetDifference(key ~ configOptionModifiedMessage, 8);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Debug logging\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"skip_file in actual config = \" ~ to!string(configFileSkipFileReadIn), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"skip_file in backup config = \" ~ to!string(skip_file_present), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"defaultSkipFile value = \" ~ to!string(defaultSkipFile), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"configFileSkipFile value = \" ~ to!string(configFileSkipFile), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"backupConfigFileSkipFile value = \" ~ to!string(backupConfigFileSkipFile), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t\t// skip_file can be specified multiple times\n\t\t\t\t\tif (skip_file_present && backupConfigFileSkipFile != configFileSkipFile) logAndSetDifference(\"skip_file\" ~ configOptionModifiedMessage, 4);\n\t\t\t\t\t\n\t\t\t\t\t// skip_file can also be an empty string, thus when removed, as an empty string, we are going back to application defaults\n\t\t\t\t\tif (skip_file_present && backupConfigFileSkipFile != defaultSkipFile) logAndSetDifference(\"skip_file\" ~ configOptionModifiedMessage, 4);\n\t\t\t\t\t\n\t\t\t\t\t// skip_dir can be specified multiple times\n\t\t\t\t\tif (skip_dir_present && backupConfigFileSkipDir != configFileSkipDir) logAndSetDifference(\"skip_dir\" ~ configOptionModifiedMessage, 5);\n\t\t\t\t\t\n\t\t\t\t\t// Check for newly added configuration options to the 'config' file vs being present in the 'backup' config file\n\t\t\t\t\tif (!drive_id_present && configFileDriveId != \"\") logAndSetDifference(\"drive_id newly added ... --resync needed\", 2);\n\t\t\t\t\tif (!sync_dir_present && configFileSyncDir != defaultSyncDir) logAndSetDifference(\"sync_dir newly added ... --resync needed\", 3);\n\t\t\t\t\tif (configFileSkipFileReadIn) {\n\t\t\t\t\t\t// We actually read a 'skip_file' configuration line from the 'config' file\n\t\t\t\t\t\tif (!skip_file_present && configFileSkipFile != defaultSkipFile) logAndSetDifference(\"skip_file newly added ... --resync needed\", 4);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Other options\n\t\t\t\t\tif (!skip_dir_present && configFileSkipDir != \"\") logAndSetDifference(\"skip_dir newly added ... --resync needed\", 5);\n\t\t\t\t\tif (!skip_dotfiles_present && configFileSkipDotfiles) logAndSetDifference(\"skip_dotfiles newly added ... --resync needed\", 6);\n\t\t\t\t\tif (!skip_symlinks_present && configFileSkipSymbolicLinks) logAndSetDifference(\"skip_symlinks newly added ... --resync needed\", 7);\n\t\t\t\t\tif (!sync_business_shared_items_present && configFileSyncBusinessSharedItems) logAndSetDifference(\"sync_business_shared_items newly added ... --resync needed\", 8);\n\t\t\t\t\tif (!check_nosync_present && configFileCheckNoSync) logAndSetDifference(\"check_nosync newly added ... --resync needed\", 9);\n\t\t\t\t\tif (!skip_size_present && configFileSkipSize) logAndSetDifference(\"skip_size newly added ... --resync needed\", 10);\n\t\t\t\t} else {\n\t\t\t\t\t// failed to read backup config file\n\t\t\t\t\taddLogEntry(\"WARNING: unable to read backup config, unable to validate if any changes made\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"WARNING: no backup config file was found, unable to validate if any changes made\");\n\t\t\t}\n\t\t}\n\t\t\n\t\t// config file set options can be changed via CLI input, specifically these will impact sync and a --resync will be needed:\n\t\t//  --syncdir ARG\n\t\t//  --skip-file ARG\n\t\t//  --skip-dir ARG\n\t\t//  --skip-dot-files\n\t\t//  --skip-symlinks\n\t\t//  --check-for-nosync\n\t\t//  --skip-size ARG\n\n\t\t// Check CLI options\n\t\tif (exists(applicableConfigFilePath)) {\n\t\t\tif (configFileSyncDir != \"\" && configFileSyncDir != getValueString(\"sync_dir\")) {\n\t\t\t\t// config file was set and CLI input changed this\n\t\t\t\t// Is this potentially running as a Docker container?\n\t\t\t\tif (entrypointExists) {\n\t\t\t\t\t// entrypoint.sh exists\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"sync_dir: CLI override of config file option, however entrypoint.sh exists, thus most likely running as a container\", [\"debug\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// Not a Docker container, raise that --resync needed due to configuration change\n\t\t\t\t\tlogAndSetDifference(\"sync_dir: CLI override of config file option, --resync needed\", 3);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (configFileSkipFile != \"\" && configFileSkipFile != getValueString(\"skip_file\")) logAndSetDifference(\"skip_file: CLI override of config file option, --resync needed\", 4);\n\t\t\tif (configFileSkipDir != \"\" && configFileSkipDir != getValueString(\"skip_dir\")) logAndSetDifference(\"skip_dir: CLI override of config file option, --resync needed\", 5);\n\t\t\tif (!configFileSkipDotfiles && getValueBool(\"skip_dotfiles\")) logAndSetDifference(\"skip_dotfiles: CLI override of config file option, --resync needed\", 6);\n\t\t\tif (!configFileSkipSymbolicLinks && getValueBool(\"skip_symlinks\")) logAndSetDifference(\"skip_symlinks: CLI override of config file option, --resync needed\", 7);\n\t\t\tif (!configFileCheckNoSync && getValueBool(\"check_nosync\")) logAndSetDifference(\"check_nosync: CLI override of config file option, --resync needed\", 9);\n\t\t\tif (!configFileSkipSize && (getValueLong(\"skip_size\") > 0)) logAndSetDifference(\"skip_size: CLI override of config file option, --resync needed\", 10);\n\t\t}\n\n\t\t// Aggregate the result to determine if a resync is required\n\t\tif (!failedToReadBackupConfig) {\n\t\t\tforeach (optionDifferent; configOptionsDifferent) {\n\t\t\t\tif (optionDifferent) {\n\t\t\t\t\tresyncRequired = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Final override\n\t\t// In certain situations, regardless of config 'resync' needed status, ignore this so that the application can display 'non-syncable' information\n\t\t// Options that should now be looked at are:\n\t\t// --list-shared-items\n\t\tif (getValueBool(\"list_business_shared_items\")) resyncRequired = false;\n\t\t\n\t\t// Return the calculated boolean\n\t\treturn resyncRequired;\n\t}\n\t\n\t// Cleanup hash files that require to be cleaned up when a --resync is issued\n\tvoid cleanupHashFilesDueToResync() {\n\t\tif (!getValueBool(\"dry_run\")) {\n\t\t\t// cleanup hash files\n\t\t\tif (debugLogging) {addLogEntry(\"Cleaning up configuration hash files\", [\"debug\"]);}\n\t\t\tsafeRemove(configHashFile);\n\t\t\tsafeRemove(syncListHashFile);\n\t\t} else {\n\t\t\t// --dry-run scenario ... technically we should not be making any local file changes .......\n\t\t\taddLogEntry(\"DRY-RUN: Not removing hash files as --dry-run has been used\");\n\t\t}\n\t}\n\t\n\t// For each of the config files, update the hash data in the hash files\n\tvoid updateHashContentsForConfigFiles() {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\t// Are we in a --dry-run scenario?\n\t\tif (!getValueBool(\"dry_run\")) {\n\t\t\t// Not a dry-run scenario, update the applicable files\n\t\t\t// Update applicable 'config' files\n\t\t\tif (exists(applicableConfigFilePath)) {\n\t\t\t\t// Update the hash of the applicable config file\n\t\t\t\tif (debugLogging) {addLogEntry(\"Updating applicable config file hash\", [\"debug\"]);}\n\t\t\t\ttry {\n\t\t\t\t\tstd.file.write(configHashFile, computeQuickXorHash(applicableConfigFilePath));\n\t\t\t\t\t// Hash file should only be readable by the user who created it - 0600 permissions needed\n\t\t\t\t\tconfigHashFile.setAttributes(convertedPermissionValue);\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// filesystem error\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, configHashFile, FsErrorSeverity.warning);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Update 'sync_list' files\n\t\t\tif (exists(syncListFilePath)) {\n\t\t\t\t// update sync_list hash\n\t\t\t\tif (debugLogging) {addLogEntry(\"Updating sync_list hash\", [\"debug\"]);}\n\t\t\t\ttry {\n\t\t\t\t\tstd.file.write(syncListHashFile, computeQuickXorHash(syncListFilePath));\n\t\t\t\t\t// Hash file should only be readable by the user who created it - 0600 permissions needed\n\t\t\t\t\tsyncListHashFile.setAttributes(convertedPermissionValue);\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// filesystem error\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, syncListHashFile, FsErrorSeverity.warning);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// --dry-run scenario ... technically we should not be making any local file changes .......\n\t\t\taddLogEntry(\"DRY-RUN: Not updating hash files as --dry-run has been used\");\n\t\t}\n\t}\n\t\n\t// Create any required hash files for files that help us determine if the configuration has changed since last run\n\tvoid createRequiredInitialConfigurationHashFiles() {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\t// Does a 'config' file exist with a valid hash file\n\t\tif (exists(applicableConfigFilePath)) {\n\t\t\tif (!exists(configHashFile)) {\n\t\t\t\t// no existing hash file exists\n\t\t\t\ttry {\n\t\t\t\t\tstd.file.write(configHashFile, \"initial-hash\");\n\t\t\t\t\t// Hash file should only be readable by the user who created it - 0600 permissions needed\n\t\t\t\t\tconfigHashFile.setAttributes(convertedPermissionValue);\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// filesystem error\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, configHashFile, FsErrorSeverity.warning);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Generate the runtime hash for the 'config' file\n\t\t\tcurrentConfigHash = computeQuickXorHash(applicableConfigFilePath);\n\t\t}\n\t\t\n\t\t// Does a 'sync_list' file exist with a valid hash file\n\t\tif (exists(syncListFilePath)) {\n\t\t\tif (!exists(syncListHashFile)) {\n\t\t\t\t// no existing hash file exists\n\t\t\t\ttry {\n\t\t\t\t\tstd.file.write(syncListHashFile, \"initial-hash\");\n\t\t\t\t\t// Hash file should only be readable by the user who created it - 0600 permissions needed\n\t\t\t\t\tsyncListHashFile.setAttributes(convertedPermissionValue);\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// filesystem error\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, syncListHashFile, FsErrorSeverity.warning);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Generate the runtime hash for the 'sync_list' file\n\t\t\tcurrentSyncListHash = computeQuickXorHash(syncListFilePath);\n\t\t}\n\t}\n\t\n\t// Read in the text values of the previous configurations\n\tint readExistingConfigurationHashFiles() {\n\t\tif (exists(configHashFile)) {\n\t\t\ttry {\n\t\t\t\tpreviousConfigHash = readText(configHashFile);\n\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t// Unable to access required hash file\n\t\t\t\taddLogEntry(\"ERROR: Unable to access \" ~ e.msg);\n\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\treturn EXIT_FAILURE;\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (exists(syncListHashFile)) {\n\t\t\ttry {\n\t\t\t\tpreviousSyncListHash = readText(syncListHashFile);\n\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t// Unable to access required hash file\n\t\t\t\taddLogEntry(\"ERROR: Unable to access \" ~ e.msg);\n\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\treturn EXIT_FAILURE;\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn 0;\n\t}\n\t\n\t// Check for basic option conflicts - flags that should not be used together and/or flag combinations that conflict with each other\n\tbool checkForBasicOptionConflicts() {\n\t\n\t\tbool operationalConflictDetected = false;\n\t\t\n\t\t// What are the permission that have been set for the application?\n\t\t// These are relevant for:\n\t\t// - The ~/OneDrive parent folder or 'sync_dir' configured item\n\t\t// - Any new folder created under ~/OneDrive or 'sync_dir'\n\t\t// - Any new file created under ~/OneDrive or 'sync_dir'\n\t\t// valid permissions are 000 -> 777 - anything else is invalid\n\t\tlong syncDirPermissions = getValueLong(\"sync_dir_permissions\");\n\t\tlong syncFilePermissions = getValueLong(\"sync_file_permissions\");\n\t\tbool invalidPermissions = false;\n\t\t\n\t\t// Check 'sync_dir_permissions'\n\t\tif (syncDirPermissions < 0 || syncDirPermissions > 777) {\n\t\t\taddLogEntry(\"ERROR: Invalid 'User|Group|Other' permissions set for 'sync_dir_permissions' within your config file. Please check your configuration\");\n\t\t\tinvalidPermissions = true;\n\t\t}\n\t\t\n\t\t// Check 'sync_file_permissions'\n\t\tif (syncFilePermissions < 0 || syncFilePermissions > 777) {\n\t\t\taddLogEntry(\"ERROR: Invalid 'User|Group|Other' permissions set for 'sync_file_permissions' within your config file. Please check your configuration\");\n\t\t\tinvalidPermissions = true;\n\t\t}\n\t\t\n\t\t// Invalid permissions detected?\n\t\tif (invalidPermissions) {\n\t\t\toperationalConflictDetected = true;\n\t\t} else {\n\t\t\t// Debug log output what permissions are being set to\n\t\t\tif (debugLogging) {addLogEntry(\"Configuring default new folder permissions as: \" ~ to!string(getValueLong(\"sync_dir_permissions\")), [\"debug\"]);}\n\t\t\tconfigureRequiredDirectoryPermissions();\n\t\t\tif (debugLogging) {addLogEntry(\"Configuring default new file permissions as: \" ~ to!string(getValueLong(\"sync_file_permissions\")), [\"debug\"]);}\n\t\t\tconfigureRequiredFilePermissions();\n\t\t}\n\t\t\n\t\t// --upload-only and --download-only cannot be used together\n\t\tif ((getValueBool(\"upload_only\")) && (getValueBool(\"download_only\"))) {\n\t\t\taddLogEntry(\"ERROR: --upload-only and --download-only cannot be used together. Use one, not both at the same time\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --sync and --monitor cannot be used together\n\t\tif ((getValueBool(\"synchronize\")) && (getValueBool(\"monitor\"))) {\n\t\t\taddLogEntry(\"ERROR: --sync and --monitor cannot be used together. Only use one of these options, not both at the same time\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --no-remote-delete can ONLY be enabled when --upload-only is used\n\t\tif ((getValueBool(\"no_remote_delete\")) && (!getValueBool(\"upload_only\"))) {\n\t\t\taddLogEntry(\"ERROR: --no-remote-delete can only be used with --upload-only\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --remove-source-files can ONLY be enabled when --upload-only is used\n\t\tif ((getValueBool(\"remove_source_files\")) && (!getValueBool(\"upload_only\"))) {\n\t\t\taddLogEntry(\"ERROR: --remove-source-files can only be used with --upload-only\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --cleanup-local-files can ONLY be enabled when --download-only is used\n\t\tif ((getValueBool(\"cleanup_local_files\")) && (!getValueBool(\"download_only\"))) {\n\t\t\taddLogEntry(\"ERROR: --cleanup-local-files can only be used with --download-only\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --list-shared-folders cannot be used with --resync and/or --resync-auth\n\t\tif ((getValueBool(\"list_business_shared_items\")) && ((getValueBool(\"resync\")) || (getValueBool(\"resync_auth\")))) {\n\t\t\taddLogEntry(\"ERROR: --list-shared-items cannot be used with --resync or --resync-auth\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --list-shared-folders cannot be used with --sync or --monitor\n\t\tif ((getValueBool(\"list_business_shared_items\")) && ((getValueBool(\"synchronize\")) || (getValueBool(\"monitor\")))) {\n\t\t\taddLogEntry(\"ERROR: --list-shared-items cannot be used with --sync or --monitor\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --sync-shared-files can ONLY be used with sync_business_shared_items\n\t\tif ((getValueBool(\"sync_business_shared_files\")) && (!getValueBool(\"sync_business_shared_items\"))) {\n\t\t\taddLogEntry(\"ERROR: The --sync-shared-files option can only be utilised if the 'sync_business_shared_items' configuration setting is enabled.\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\t\t\n\t\t// --display-sync-status cannot be used with --resync and/or --resync-auth\n\t\tif ((getValueBool(\"display_sync_status\")) && ((getValueBool(\"resync\")) || (getValueBool(\"resync_auth\")))) {\n\t\t\taddLogEntry(\"ERROR: --display-sync-status cannot be used with --resync or --resync-auth\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --modified-by cannot be used with --resync and/or --resync-auth\n\t\tif ((!getValueString(\"modified_by\").empty) && ((getValueBool(\"resync\")) || (getValueBool(\"resync_auth\")))) {\n\t\t\taddLogEntry(\"ERROR: --modified-by cannot be used with --resync or --resync-auth\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --get-file-link cannot be used with --resync and/or --resync-auth\n\t\tif ((!getValueString(\"get_file_link\").empty) && ((getValueBool(\"resync\")) || (getValueBool(\"resync_auth\")))) {\n\t\t\taddLogEntry(\"ERROR: --get-file-link cannot be used with --resync or --resync-auth\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --create-share-link cannot be used with --resync and/or --resync-auth\n\t\tif ((!getValueString(\"create_share_link\").empty) && ((getValueBool(\"resync\")) || (getValueBool(\"resync_auth\")))) {\n\t\t\taddLogEntry(\"ERROR: --create-share-link cannot be used with --resync or --resync-auth\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --get-sharepoint-drive-id cannot be used with --resync and/or --resync-auth\n\t\tif ((!getValueString(\"sharepoint_library_name\").empty) && ((getValueBool(\"resync\")) || (getValueBool(\"resync_auth\")))) {\n\t\t\taddLogEntry(\"ERROR: --get-sharepoint-drive-id cannot be used with --resync or --resync-auth\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --monitor and --display-sync-status cannot be used together\n\t\tif ((getValueBool(\"monitor\")) && (getValueBool(\"display_sync_status\"))) {\n\t\t\taddLogEntry(\"ERROR: --monitor and --display-sync-status cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --sync and --display-sync-status cannot be used together\n\t\tif ((getValueBool(\"synchronize\")) && (getValueBool(\"display_sync_status\"))) {\n\t\t\taddLogEntry(\"ERROR: --sync and --display-sync-status cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --monitor and --display-quota cannot be used together\n\t\tif ((getValueBool(\"monitor\")) && (getValueBool(\"display_quota\"))) {\n\t\t\taddLogEntry(\"ERROR: --monitor and --display-quota cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --sync and --display-quota cannot be used together\n\t\tif ((getValueBool(\"synchronize\")) && (getValueBool(\"display_quota\"))) {\n\t\t\taddLogEntry(\"ERROR: --sync and --display-quota cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\t\t\n\t\t// --force-sync can only be used when using --sync and --single-directory\n\t\tif (getValueBool(\"force_sync\")) {\n\t\t\n\t\t\tbool conflict = false;\n\t\t\t// Should not be used with --monitor\n\t\t\tif (getValueBool(\"monitor\")) conflict = true;\n\t\t\t// single_directory must not be empty\n\t\t\tif (getValueString(\"single_directory\").empty) conflict = true;\n\t\t\tif (conflict) {\n\t\t\t\taddLogEntry(\"ERROR: --force-sync can only be used with --sync --single-directory\");\n\t\t\t\toperationalConflictDetected = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// When using 'azure_ad_endpoint', 'azure_tenant_id' cannot be empty\n\t\tif ((!getValueString(\"azure_ad_endpoint\").empty) && (getValueString(\"azure_tenant_id\").empty)) {\n\t\t\taddLogEntry(\"ERROR: config option 'azure_tenant_id' cannot be empty when 'azure_ad_endpoint' is configured\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// When using --enable-logging the 'log_dir' cannot be empty\n\t\tif ((getValueBool(\"enable_logging\")) && (getValueString(\"log_dir\").empty)) {\n\t\t\taddLogEntry(\"ERROR: config option 'log_dir' cannot be empty when 'enable_logging' is configured\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// When using --syncdir, the value cannot be empty.\n\t\tif (strip(getValueString(\"sync_dir\")).empty) {\n\t\t\taddLogEntry(\"ERROR: --syncdir value cannot be empty\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --monitor and --create-directory cannot be used together\n\t\tif ((getValueBool(\"monitor\")) && (!getValueString(\"create_directory\").empty)) {\n\t\t\taddLogEntry(\"ERROR: --monitor and --create-directory cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --sync and --create-directory cannot be used together\n\t\tif ((getValueBool(\"synchronize\")) && (!getValueString(\"create_directory\").empty)) {\n\t\t\taddLogEntry(\"ERROR: --sync and --create-directory cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --monitor and --remove-directory cannot be used together\n\t\tif ((getValueBool(\"monitor\")) && (!getValueString(\"remove_directory\").empty)) {\n\t\t\taddLogEntry(\"ERROR: --monitor and --remove-directory cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --sync and --remove-directory cannot be used together\n\t\tif ((getValueBool(\"synchronize\")) && (!getValueString(\"remove_directory\").empty)) {\n\t\t\taddLogEntry(\"ERROR: --sync and --remove-directory cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --monitor and --source-directory cannot be used together\n\t\tif ((getValueBool(\"monitor\")) && (!getValueString(\"source_directory\").empty)) {\n\t\t\taddLogEntry(\"ERROR: --monitor and --source-directory cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --sync and --source-directory cannot be used together\n\t\tif ((getValueBool(\"synchronize\")) && (!getValueString(\"source_directory\").empty)) {\n\t\t\taddLogEntry(\"ERROR: --sync and --source-directory cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --monitor and --destination-directory cannot be used together\n\t\tif ((getValueBool(\"monitor\")) && (!getValueString(\"destination_directory\").empty)) {\n\t\t\taddLogEntry(\"ERROR: --monitor and --destination-directory cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --sync and --destination-directory cannot be used together\n\t\tif ((getValueBool(\"synchronize\")) && (!getValueString(\"destination_directory\").empty)) {\n\t\t\taddLogEntry(\"ERROR: --sync and --destination-directory cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --download-only and --local-first cannot be used together\n\t\tif ((getValueBool(\"download_only\")) && (getValueBool(\"local_first\"))) {\n\t\t\taddLogEntry(\"ERROR: --download-only cannot be used with --local-first\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// Test that '--modified-by <arg>' has a valid argument and not another directive\n\t\tif (getValueString(\"modified_by\") != \"\") {\n\t\t\t// Does the string start with '--' ?\n\t\t\tif (getValueString(\"modified_by\").startsWith(\"--\")) {\n\t\t\t\taddLogEntry(\"ERROR: --modified-by missing a valid entry\");\n\t\t\t\toperationalConflictDetected = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Test that '--get-file-link <arg>' has a valid argument and not another directive\n\t\tif (getValueString(\"get_file_link\") != \"\") {\n\t\t\t// Does the string start with '--' ?\n\t\t\tif (getValueString(\"get_file_link\").startsWith(\"--\")) {\n\t\t\t\taddLogEntry(\"ERROR: --get-file-link missing a valid entry\");\n\t\t\t\toperationalConflictDetected = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Test that '--create-share-link <arg>' has a valid argument and not another directive\n\t\tif (getValueString(\"create_share_link\") != \"\") {\n\t\t\t// Does the string start with '--' ?\n\t\t\tif (getValueString(\"create_share_link\").startsWith(\"--\")) {\n\t\t\t\taddLogEntry(\"ERROR: --create-share-link missing a valid entry\");\n\t\t\t\toperationalConflictDetected = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Test that '--create-directory <arg>' has a valid argument and not another directive\n\t\tif (getValueString(\"create_directory\") != \"\") {\n\t\t\t// Does the string start with '--' ?\n\t\t\tif (getValueString(\"create_directory\").startsWith(\"--\")) {\n\t\t\t\taddLogEntry(\"ERROR: --create-directory missing a valid entry\");\n\t\t\t\toperationalConflictDetected = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Test that '--remove-directory <arg>' has a valid argument and not another directive\n\t\tif (getValueString(\"remove_directory\") != \"\") {\n\t\t\t// Does the string start with '--' ?\n\t\t\tif (getValueString(\"remove_directory\").startsWith(\"--\")) {\n\t\t\t\taddLogEntry(\"ERROR: --remove-directory missing a valid entry\");\n\t\t\t\toperationalConflictDetected = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Test that '--source-directory <arg>' has a valid argument and not another directive\n\t\tif (getValueString(\"source_directory\") != \"\") {\n\t\t\t// Does the string start with '--' ?\n\t\t\tif (getValueString(\"source_directory\").startsWith(\"--\")) {\n\t\t\t\taddLogEntry(\"ERROR: --source-directory missing a valid entry\");\n\t\t\t\toperationalConflictDetected = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Test that '--destination-directory <arg>' has a valid argument and not another directive\n\t\tif (getValueString(\"destination_directory\") != \"\") {\n\t\t\t// Does the string start with '--' ?\n\t\t\tif (getValueString(\"destination_directory\").startsWith(\"--\")) {\n\t\t\t\taddLogEntry(\"ERROR: --destination-directory missing a valid entry\");\n\t\t\t\toperationalConflictDetected = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// 'use_intune_sso' and 'use_device_auth' cannot be used together\n\t\tif ((getValueBool(\"use_intune_sso\")) && (getValueBool(\"use_device_auth\"))) {\n\t\t\taddLogEntry(\"ERROR: 'use_intune_sso' and 'use_device_auth' cannot be used together\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\n\t\t// --force and --resync cannot be used together as --resync blows away the database, thus there is no way to calculate large local deletes\n\t\tif ((getValueBool(\"force\")) && (getValueBool(\"resync\"))) {\n\t\t\taddLogEntry(\"ERROR: --force and --resync cannot be used together as there is zero way to determine that a big delete has occurred\");\n\t\t\toperationalConflictDetected = true;\n\t\t}\n\t\t\t\t\n\t\t// Return bool value indicating if we have an operational conflict\n\t\treturn operationalConflictDetected;\n\t}\n\t\n\t// Reset skip_file and skip_dir to application defaults when --force-sync is used\n\tvoid resetSkipToDefaults() {\n\t\t// skip_file\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"original skip_file: \" ~ getValueString(\"skip_file\"), [\"debug\"]);\n\t\t\taddLogEntry(\"resetting skip_file to application defaults\", [\"debug\"]);\n\t\t}\n\t\tsetValueString(\"skip_file\", defaultSkipFile);\n\t\tif (debugLogging) {addLogEntry(\"reset skip_file: \" ~ getValueString(\"skip_file\"), [\"debug\"]);}\n\t\t\n\t\t// skip_dir\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"original skip_dir: \" ~ getValueString(\"skip_dir\"), [\"debug\"]);\n\t\t\taddLogEntry(\"resetting skip_dir to application defaults\", [\"debug\"]);\n\t\t}\n\t\tsetValueString(\"skip_dir\", defaultSkipDir);\n\t\tif (debugLogging) {addLogEntry(\"reset skip_dir: \" ~ getValueString(\"skip_dir\"), [\"debug\"]);}\n\t}\n\t\n\t// Initialise the correct 'sync_dir' expanding any '~' if present\n\tstring initialiseRuntimeSyncDirectory() {\n\t\t// Log what we are doing\n\t\tif (debugLogging) {addLogEntry(\"sync_dir: Setting runtimeSyncDirectory from config value 'sync_dir'\", [\"debug\"]);}\n\t\t\n\t\tif (!shellEnvironmentSet){\n\t\t\tif (debugLogging) {addLogEntry(\"sync_dir: No SHELL or USER environment variable configuration detected\", [\"debug\"]);}\n\t\t\t\n\t\t\t// No shell or user set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker\n\t\t\t// Does the 'currently configured' sync_dir include a ~\n\t\t\tif (canFind(getValueString(\"sync_dir\"), \"~\")) {\n\t\t\t\t// A ~ was found in sync_dir\n\t\t\t\tif (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\"]);}\n\t\t\t\truntimeSyncDirectory = buildNormalizedPath(buildPath(defaultHomePath, strip(getValueString(\"sync_dir\"), \"~\")));\n\t\t\t} else {\n\t\t\t\t// No ~ found in sync_dir, use as is\n\t\t\t\tif (debugLogging) {addLogEntry(\"sync_dir: Using configured 'sync_dir' path as-is as no SHELL or USER environment variable configuration detected\", [\"debug\"]);}\n\t\t\t\truntimeSyncDirectory = getValueString(\"sync_dir\");\n\t\t\t}\n\t\t} else {\n\t\t\t// A shell and user environment variable is set, expand any ~ as this will be expanded correctly if present\n\t\t\tif (canFind(getValueString(\"sync_dir\"), \"~\")) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"sync_dir: A '~' was found in the configured 'sync_dir', automatically expanding as SHELL and USER environment variable is set\", [\"debug\"]);}\n\t\t\t\truntimeSyncDirectory = expandTilde(getValueString(\"sync_dir\"));\n\t\t\t} else {\n\t\t\t\t// No ~ found in sync_dir, does the path begin with a '/' ?\n\t\t\t\tif (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\"]);}\n\t\t\t\tif (!startsWith(getValueString(\"sync_dir\"), \"/\")) {\n\t\t\t\t\tif (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\"]);}\n\t\t\t\t\tstring updatedPathWithHome = \"~/\" ~ getValueString(\"sync_dir\");\n\t\t\t\t\truntimeSyncDirectory = expandTilde(updatedPathWithHome);\n\t\t\t\t} else {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"use 'sync_dir' as is - no touch\", [\"debug\"]);}\n\t\t\t\t\truntimeSyncDirectory = getValueString(\"sync_dir\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// What will runtimeSyncDirectory be actually set to?\n\t\tif (debugLogging) {addLogEntry(\"sync_dir: runtimeSyncDirectory set to: \" ~ runtimeSyncDirectory, [\"debug\"]);}\n\t\t\n\t\t// Configure configuredBusinessSharedFilesDirectoryName\n\t\tconfiguredBusinessSharedFilesDirectoryName = buildNormalizedPath(buildPath(runtimeSyncDirectory, defaultBusinessSharedFilesDirectoryName));\n\t\t\n\t\treturn runtimeSyncDirectory;\n\t}\n\t\n\t// Initialise the correct 'log_dir' when application logging to a separate file is enabled with 'enable_logging' and expanding any '~' if present\n\tstring calculateLogDirectory() {\n\t\tstring configuredLogDirPath;\n\n\t\tif (debugLogging) {addLogEntry(\"log_dir: Setting runtime application log from config value 'log_dir'\", [\"debug\"]);}\n\n\t\tif (getValueString(\"log_dir\") != defaultLogFileDir) {\n\t\t\t// User modified 'log_dir' to be used with 'enable_logging'\n\t\t\t// if 'log_dir' contains a '~' this needs to be expanded correctly\n\t\t\tif (canFind(getValueString(\"log_dir\"), \"~\")) {\n\t\t\t\t// ~ needs to be expanded correctly\n\t\t\t\tif (!shellEnvironmentSet) {\n\t\t\t\t\t// No shell or user environment variable set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"log_dir: A '~' was found in 'log_dir' however '~' as no SHELL or USER environment variable set\", [\"debug\"]);}\n\t\t\t\t\tconfiguredLogDirPath = buildNormalizedPath(buildPath(defaultHomePath, strip(getValueString(\"log_dir\"), \"~\")));\n\t\t\t\t} else {\n\t\t\t\t\t// We have a SHELL or USER environment variable set, so expandTilde() should work correctly\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"log_dir: A '~' was found in the configured 'log_dir', automatically expanding as SHELL and USER environment variable is set\", [\"debug\"]);}\n\t\t\t\t\tconfiguredLogDirPath = buildNormalizedPath(expandTilde(getValueString(\"log_dir\")));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No '~' present - use as-is, but normalise\n\t\t\t\tconfiguredLogDirPath = buildNormalizedPath(getValueString(\"log_dir\"));\n\t\t\t}\n\t\t} else {\n\t\t\t// Default 'log_dir' to be used with 'enable_logging'\n\t\t\tconfiguredLogDirPath = defaultLogFileDir;\n\t\t}\n\n\t\t// Attempt to create 'configuredLogDirPath' if this does not exist, otherwise we need to fall back to the users home directory\n\t\tif (!exists(configuredLogDirPath)) {\n\t\t\t// 'configuredLogDirPath' path does not exist - try and create it\n\t\t\ttry {\n\t\t\t\tmkdirRecurse(configuredLogDirPath);\n\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t// We got an error when attempting to create the directory ..\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: Unable to create \" ~ configuredLogDirPath);\n\t\t\t\taddLogEntry(\"ERROR: Please manually create '\" ~ configuredLogDirPath ~ \"' and ensure that the permissions allow write access for your user to this location.\");\n\t\t\t\taddLogEntry(\"ERROR: The requested client activity log will instead be located in your users home directory\");\n\t\t\t\taddLogEntry();\n\n\t\t\t\t// Reconfigure 'configuredLogDirPath' to use environment.get(\"HOME\") value, which we have already calculated\n\t\t\t\tconfiguredLogDirPath = defaultHomePath;\n\t\t\t}\n\t\t}\n\n\t\t// Verify that we can actually write in configuredLogDirPath\n\t\t// If we cannot, fall back to the user's home directory instead of later crashing\n\t\ttry {\n\t\t\tauto testFile = buildNormalizedPath(buildPath(configuredLogDirPath, \".onedrive_log_write_test\"));\n\t\t\t// Try to append a zero-length string – this will create the file if possible\n\t\t\tstd.file.append(testFile, \"\");\n\t\t\t// Clean up the test file\n\t\t\tstd.file.remove(testFile);\n\t\t} catch (std.file.FileException e) {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"ERROR: Unable to write to \" ~ configuredLogDirPath);\n\t\t\taddLogEntry(\"ERROR: Please manually adjust permissions or choose a different 'log_dir' in the configuration file.\");\n\t\t\taddLogEntry(\"ERROR: The requested client activity log will instead be located in your users home directory\");\n\t\t\taddLogEntry();\n\n\t\t\t// Reconfigure 'configuredLogDirPath' to use environment.get(\"HOME\") value, which we have already calculated\n\t\t\tconfiguredLogDirPath = defaultHomePath;\n\t\t}\n\n\t\t// Return the initialised application log path\n\t\treturn configuredLogDirPath;\n\t}\n\n\t// What IP protocol is going to be used to access Microsoft OneDrive\n\tvoid displayIPProtocol() {\n\t\tif (getValueLong(\"ip_protocol_version\") == 0) addLogEntry(\"Using IPv4 and IPv6 (if configured) for all network operations\");\n\t\tif (getValueLong(\"ip_protocol_version\") == 1) addLogEntry(\"Forcing client to use IPv4 connections only\");\n\t\tif (getValueLong(\"ip_protocol_version\") == 2) addLogEntry(\"Forcing client to use IPv6 connections only\");\n\t}\n\t\n\t// Has a 'no-sync' task been requested?\n\tbool hasNoSyncOperationBeenRequested() {\n\t\t// Are we performing some sort of 'no-sync' task?\n\t\t// - Are we performing a logout?\n\t\t// - Are we performing a reauth?\n\t\t// - Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library?\n\t\t// - Are we displaying the sync status?\n\t\t// - Are we getting the URL for a file online?\n\t\t// - Are we listing who modified a file last online?\n\t\t// - Are we listing OneDrive Business Shared Items?\n\t\t// - Are we creating a shareable link for an existing file on OneDrive?\n\t\t// - Are we just creating a directory online, without any sync being performed?\n\t\t// - Are we just deleting a directory online, without any sync being performed?\n\t\t// - Are we renaming or moving a directory?\n\t\t// - Are we displaying the quota information?\n\t\t// - Are we downloading a single file?\n\t\tbool noSyncOperation = false;\n\t\t\n\t\t// 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\n\t\t\n\t\t// --logout\n\t\tif (getValueBool(\"logout\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --reauth\n\t\tif (getValueBool(\"reauth\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --get-sharepoint-drive-id - Get the SharePoint Library drive_id\n\t\tif (getValueString(\"sharepoint_library_name\") != \"\") {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --display-sync-status - Query the sync status\n\t\tif (getValueBool(\"display_sync_status\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --get-file-link - Get the URL path for a synced file?\n\t\tif (getValueString(\"get_file_link\") != \"\") {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --modified-by - Are we listing the modified-by details of a provided path?\n\t\tif (getValueString(\"modified_by\") != \"\") {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --list-shared-items - Are we listing OneDrive Business Shared Items\n\t\tif (getValueBool(\"list_business_shared_items\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --create-share-link - Are we creating a shareable link for an existing file on OneDrive?\n\t\tif (getValueString(\"create_share_link\") != \"\") {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --create-directory - Are we just creating a directory online, without any sync being performed?\n\t\tif ((getValueString(\"create_directory\") != \"\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// --remove-directory - Are we just deleting a directory online, without any sync being performed?\n\t\tif ((getValueString(\"remove_directory\") != \"\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// Are we renaming or moving a directory online?\n\t\t// \tonedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'\n\t\tif ((getValueString(\"source_directory\") != \"\") && (getValueString(\"destination_directory\") != \"\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// Are we displaying the quota information?\n\t\tif (getValueBool(\"display_quota\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// Are we downloading a single file?\n\t\tif ((getValueString(\"download_single_file\") != \"\")) {\n\t\t\t// flag that a no sync operation has been requested\n\t\t\tnoSyncOperation = true;\n\t\t}\n\t\t\n\t\t// Return result\n\t\treturn noSyncOperation;\n\t}\n\t\n\t// Are the required GUI logging environment variables for this user available?\n\t// Specifically these must be available:\n\t// - XDG_RUNTIME_DIR\n\t// - DBUS_SESSION_BUS_ADDRESS\n\tbool validateGUINotificationEnvironmentVariables() {\n\t\n\t\tbool variablesAvailable = false;\n\t\tstring xdg_value;\n\t\tstring dbus_value;\n\t\t\n\t\tversion(Notifications) {\n\t\t\t// Check XDG_RUNTIME_DIR environment variable\n\t\t\ttry {\n\t\t\t\txdg_value = environment[\"XDG_RUNTIME_DIR\"];\n\t\t\t\txdg_exists = true;\n\t\t\t} catch (Exception e) {\n\t\t\t\txdg_exists = false;\n\t\t\t}\n\t\t\t\n\t\t\t// Check DBUS_SESSION_BUS_ADDRESS environment variable\n\t\t\ttry {\n\t\t\t\tdbus_value = environment[\"DBUS_SESSION_BUS_ADDRESS\"];\n\t\t\t\tdbus_exists = true;\n\t\t\t} catch (Exception e) {\n\t\t\t\tdbus_exists = false;\n\t\t\t}\n\t\t\t\n\t\t\t// Output the result\n\t\t\tif (xdg_exists) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"runtime_environment: XDG_RUNTIME_DIR exists with value: \" ~ xdg_value , [\"debug\"]);}\n\t\t\t} else {\n\t\t\t\tif (debugLogging) {addLogEntry(\"runtime_environment: XDG_RUNTIME_DIR missing from runtime user environment\", [\"debug\"]);}\n\t\t\t}\n\t\t\t\n\t\t\tif (dbus_exists) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"runtime_environment: DBUS_SESSION_BUS_ADDRESS exists with value: \" ~ dbus_value, [\"debug\"]);}\n\t\t\t} else {\n\t\t\t\tif (debugLogging) {addLogEntry(\"runtime_environment: DBUS_SESSION_BUS_ADDRESS missing from runtime user environment\", [\"debug\"]);}\n\t\t\t}\n\n\t\t\t// Determine result\n\t\t\tif (xdg_exists && dbus_exists) {\n\t\t\t\tvariablesAvailable = true;\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"WARNING: Required environment variables required to enable GUI Notifications are not present\");\n\t\t\t\tvariablesAvailable = false;\n\t\t\t}\t\n\t\t}\n\n\t\t// Return result\n\t\treturn variablesAvailable;\n\t}\n\t\n\t// Set the Recycle Bin Paths\n\tvoid setRecycleBinPaths() {\n\t\tstring configured = getValueString(\"recycle_bin_path\");\n\t\tstring basePath;\n\t\tstring dirSeparatorString = \"/\";\n\n\t\t// Handle the \"no shell / no user\" case similarly to sync_dir\n\t\tif (!shellEnvironmentSet) {\n\t\t\t// No SHELL or USER means expandTilde() will fail if '~' is present\n\t\t\tif (canFind(configured, \"~\")) {\n\t\t\t\t// Replace '~' with defaultHomePath explicitly\n\t\t\t\tbasePath = buildNormalizedPath(\n\t\t\t\t\tbuildPath(defaultHomePath, strip(configured, \"~\"))\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tbasePath = configured;\n\t\t\t}\n\t\t} else {\n\t\t\t// Normal case: shell + user are set; we can rely on expandTilde()\n\t\t\tif (canFind(configured, \"~\")) {\n\t\t\t\tbasePath = expandTilde(configured);\n\t\t\t} else {\n\t\t\t\tbasePath = configured;\n\t\t\t}\n\t\t}\n\n\t\t// Make sure it's normalised and has a trailing '/'\n\t\tbasePath = buildNormalizedPath(basePath);\n\t\tif (!basePath.endsWith(dirSeparatorString)) {\n\t\t\tbasePath ~= dirSeparatorString;\n\t\t}\n\t\t\n\t\t// Update Recycle Bin paths\n\t\trecycleBinParentPath = basePath;\n\t\trecycleBinFilePath = basePath ~ \"files\" ~ dirSeparatorString;\n\t\trecycleBinInfoPath = basePath ~ \"info\"  ~ dirSeparatorString;\n\t}\n\t\n\t// Is 'recycleBinParentPath' a child path of the configured 'runtimeSyncDirectory'?\n\tbool checkRecycleBinPathAsChildOfSyncDir() {\n\t\t// Configure the variables to check\n\t\tstring syncRoot = runtimeSyncDirectory;\n\t\tstring recycleBin = recycleBinParentPath;\n\t\tstring sep = \"/\";\n\t\t\n\t\t// Make prefix check robust – ensure syncRoot ends with separator\n\t\tif (!syncRoot.endsWith(sep)) {\n\t\t\tsyncRoot ~= sep;\n\t\t}\n\t\t\n\t\t// Make prefix check robust – ensure recycleBin ends with separator\n\t\tif (!recycleBin.endsWith(sep)) {\n\t\t\trecycleBin ~= sep;\n\t\t}\n\t\t\n\t\t// Perform the check and return the evaluation\n\t\treturn startsWith(recycleBin, syncRoot);\n\t}\n\t\n\t// Is the client running under a GUI session?\n\t// - GNOME\n\t// - KDE\n\tbool isGuiSessionDetected() {\n\t\n\t\tbool hasDisplay = false;\n\t\tbool hasRuntime = false;\n\t\tbool uidMatches = false;\n\t\tbool homeOK = false;\n\t\tstring xdgType;\n\n\t\ttry {\n\t\t\txdgType = environment.get(\"XDG_SESSION_TYPE\", \"\");\n\t\t} catch (Exception e) {\n\t\t\txdgType = \"\";\n\t\t}\n\t\t\n\t\ttry {\n\t\t\thasDisplay = environment.get(\"WAYLAND_DISPLAY\", \"\").length > 0 || environment.get(\"DISPLAY\", \"\").length > 0;\n\t\t} catch (Exception e) {\n\t\t\thasDisplay = false;\n\t\t}\n\t\t\n\t\ttry {\n\t\t\thasRuntime = environment.get(\"XDG_RUNTIME_DIR\", \"\").length > 0;\n\t\t} catch (Exception e) {\n\t\t\thasRuntime = false;\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tuidMatches = (geteuid() == getuid());\n\t\t} catch (Exception e) {\n\t\t\tuidMatches = false;\n\t\t}\n\t\t\n\t\ttry {\n\t\t\thomeOK = environment.get(\"HOME\", \"\").length > 0;\n\t\t} catch (Exception e) {\n\t\t\thomeOK = false;\n\t\t}\n\t\t\n\t\tbool hasGuiElements = hasDisplay || (xdgType == \"wayland\" || xdgType == \"x11\");\n\t\t\n\t\treturn hasGuiElements && hasRuntime && uidMatches && homeOK;\n\t}\n\t\n\t// Attempt to detect the running display manager\n\tDesktopHints detectDesktop() {\n\t\tstring all = (  environment.get(\"XDG_CURRENT_DESKTOP\",\"\") ~ \":\" ~\n\t\t\t\t\t\tenvironment.get(\"XDG_SESSION_DESKTOP\",\"\") ~ \":\" ~\n\t\t\t\t\t\tenvironment.get(\"DESKTOP_SESSION\",\"\") ~ \":\" ~\n\t\t\t\t\t\tenvironment.get(\"GDMSESSION\",\"\") ~ \":\" ~\n\t\t\t\t\t\tenvironment.get(\"KDE_FULL_SESSION\",\"\")).toLower();\n\t\t\n\t\tDesktopHints hints;\n\t\thints.gnome = all.canFind(\"gnome\");\n\t\thints.kde   = all.canFind(\"kde\") || all.canFind(\"plasma\");\n\t\treturn hints;\n\t}\n\t\n\t// Generate the correct file:// URI for display manager integration\n\tstring fileUriFor(string absPath) {\n\t\t// Basic, safe URI for local file\n\t\treturn \"file://\" ~ expandTilde(absPath);\n\t}\n\t\n\t// Add GNOME Bookmark\n\tvoid addGnomeBookmark() {\n\t\t// Configure required variables\n\t\tstring uri = fileUriFor(getValueString(\"sync_dir\"));\n\t\tstring bookmarksPath = buildPath(expandTilde(environment.get(\"HOME\", \"\")), \".config\", \"gtk-3.0\", \"bookmarks\");\n\t\t\n\t\t// Ensure the bookmarks path exists\n\t\ttry {\n\t\t\t// Attempt bookmarks path creation\n\t\t\tmkdirRecurse(dirName(bookmarksPath));\n\t\t} catch (std.file.FileException e) {\n\t\t\t// Creating the bookmarks path failed\n\t\t\taddLogEntry(\"ERROR: Unable to create the GNOME bookmark directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Does the bookmark already exist?\n\t\tstring content = exists(bookmarksPath) ? readText(bookmarksPath) : \"\";\n\t\tbool present = false;\n\t\tforeach (line; content.splitLines()) {\n\t\t\tif (line.strip == uri) { present = true; break; }\n\t\t}\n\t\tif (present) return;\n\t\t\n\t\t// Append newline if needed, then our URI\n\t\tstring newline = content.length && !content.endsWith(\"\\n\") ? \"\\n\" : \"\";\n\t\tstring updated = content ~ newline ~ uri ~ \"\\n\";\n\n\t\t// Atomic write\n\t\tstring tmp = bookmarksPath ~ \".tmp\";\n\t\tstd.file.write(tmp, updated);\n\t\trename(tmp, bookmarksPath);\n\t\t\n\t\t// Log outcome\n\t\taddLogEntry(\"GNOME Desktop Integration: Bookmark added successfully\", [\"info\"]);\n\t}\n\t\n\t// Set the correct folder icon for the 'sync_dir' path\n\tvoid setOneDriveFolderIcon() {\n\t\t// Get the sync directory\n\t\tstring syncDir = expandTilde(getValueString(\"sync_dir\"));\n\t\n\t\t// Build gio command\n\t\tstring[] gioCmd = [\n\t\t\t\"gio\",\n\t\t\t\"set\",\n\t\t\tsyncDir,\n\t\t\t\"metadata::custom-icon-name\",\n\t\t\t\"onedrive\"\n\t\t];\n\t\t\n\t\t// Try and set folder icon\n\t\ttry {\n\t\t\tauto p = spawnProcess(gioCmd);\n\t\t\tint status = p.wait();\n\t\t\tif (status == 0) {\n\t\t\t\taddLogEntry(\"GNOME Desktop Integration: Set folder icon to 'onedrive' for \" ~ syncDir, [\"info\"]);\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"GNOME Desktop Integration: Failed to set folder icon for \" ~ syncDir ~ \" (gio exit \" ~ status.to!string ~ \")\", [\"info\"]);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\taddLogEntry(\"GNOME Desktop Integration: Exception setting folder icon: \" ~ e.msg, [\"info\"]);\n\t\t}\n\t}\n\t\n\t// Remove GNOME Bookmark\n\tvoid removeGnomeBookmark() {\n\t\t// Configure required variables\n\t\tstring uri = fileUriFor(getValueString(\"sync_dir\"));\n\t\tstring bookmarksPath = buildPath(expandTilde(environment.get(\"HOME\", \"\")), \".config\", \"gtk-3.0\", \"bookmarks\");\n\n\t\t// Does the bookmark path exist?\n\t\tif (!exists(bookmarksPath)) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Read existing bookmarks\n\t\tstring content = readText(bookmarksPath);\n\t\tauto lines = content.splitLines();\n\n\t\tbool changed = false;\n\t\tstring[] kept;\n\t\tkept.reserve(lines.length);\n\n\t\tforeach (line; lines) {\n\t\t\t// Remove every line that exactly matches the URI (after stripping whitespace)\n\t\t\tif (line.strip == uri) {\n\t\t\t\tchanged = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tkept ~= line;\n\t\t}\n\n\t\tif (!changed) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Rebuild file (ensure trailing newline if non-empty)\n\t\tstring updated = kept.length ? kept.join(\"\\n\") ~ \"\\n\" : \"\";\n\n\t\t// Atomic write\n\t\tconst string tmp = bookmarksPath ~ \".tmp\";\n\t\tstd.file.write(tmp, updated);\n\t\trename(tmp, bookmarksPath);\n\n\t\t// Log outcome\n\t\taddLogEntry(\"GNOME Desktop Integration: Bookmark removed successfully\", [\"info\"]);\n\t}\n\t\n\t// Remove folder icon\n\tvoid removeOneDriveFolderIcon() {\n\t\t// Get the sync directory\n\t\tstring syncDir = expandTilde(getValueString(\"sync_dir\"));\n\t\n\t\t// Build gio command\n\t\tstring[] gioCmd = [\n\t\t\t\"gio\",\n\t\t\t\"set\",\n\t\t\tsyncDir,\n\t\t\t\"metadata::custom-icon-name\",\n\t\t\t\"folder\"\n\t\t];\n\t\t\n\t\t// Try and set folder icon\n\t\ttry {\n\t\t\tauto p = spawnProcess(gioCmd);\n\t\t\tint status = p.wait();\n\t\t\tif (status == 0) {\n\t\t\t\taddLogEntry(\"GNOME Desktop Integration: Reset folder icon to 'default' for \" ~ syncDir, [\"info\"]);\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"GNOME Desktop Integration: Failed to reset folder icon for \" ~ syncDir ~ \" (gio exit \" ~ status.to!string ~ \")\", [\"info\"]);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\taddLogEntry(\"GNOME Desktop Integration: Exception setting folder icon: \" ~ e.msg, [\"info\"]);\n\t\t}\n\t}\n\t\n\t// Add KDE Places entry\n\tvoid addKDEPlacesEntry() {\n\t\t// Configure required variables\n\t\tstring uri = fileUriFor(getValueString(\"sync_dir\"));\n\t\tstring xbelPath = buildPath(expandTilde(environment.get(\"HOME\", \"\")), \".local\", \"share\", \"user-places.xbel\");\n\t\tstring content;\n\t\t\n\t\t// Ensure the xbelPath path exists\n\t\ttry {\n\t\t\t// Attempt xbelPath creation\n\t\t\tmkdirRecurse(dirName(xbelPath));\n\t\t} catch (std.file.FileException e) {\n\t\t\t// Creating the xbelPath path failed\n\t\t\taddLogEntry(\"ERROR: Unable to create the KDE Places directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Does the xbel file exist?\t\t\n\t\tif (exists(xbelPath)) {\n\t\t\t// Path exists - read the file\n\t\t\tcontent = readText(xbelPath);\n\t\t\t\n\t\t\t// Does the 'sync_dir' path exist in the xbel file?\n\t\t\tif (content.canFind(`href=\"` ~ uri ~ `\"`)) {\n\t\t\t\treturn; // already present\n\t\t\t}\n\t\t} else {\n\t\t\t// xbel path does not exist, create minimal XBEL skeleton\n\t\t\tcontent = \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\n\t\t\t\t\t   <xbel version=\\\"1.0\\\">\n\t\t\t\t\t   </xbel>\";\n\t\t}\n\n\t\t// Insert xbel bookmark before closing tag\n\t\tstring bookmark = ` <bookmark href=\"` ~ uri ~ `\">\n\t\t<title>OneDrive</title>\n\t\t<info>\n\t\t  <metadata owner=\"http://www.freedesktop.org\">\n\t\t\t<bookmark:icon name=\"onedrive\"/>\n\t\t  </metadata>\n\t\t</info>\n\t  </bookmark>`;\n\t\t\n\t\t// Update xbel file with Microsoft OneDrive Bookmark\n\t\tstring updated;\n\t\tauto idx = content.lastIndexOf(\"</xbel>\");\n\t\tif (idx >= 0) {\n\t\t\tupdated = content[0 .. idx] ~ bookmark ~ \"\\n\" ~ content[idx .. $];\n\t\t} else {\n\t\t\t// Fallback: append (still valid for many parsers)\n\t\t\tupdated = content ~ \"\\n\" ~ bookmark ~ \"\\n</xbel>\\n\";\n\t\t}\n\n\t\tstring tmp = xbelPath ~ \".tmp\";\n\t\tstd.file.write(tmp, updated);\n\t\trename(tmp, xbelPath);\n\t\t\n\t\t// Log outcome\n\t\taddLogEntry(\"KDE Desktop Integration: KDE/Plasma place added successfully\", [\"info\"]);\n\t}\n\t\t\n\t// Remove KDE Places entry\n\tvoid removeKDEPlacesEntry() {\n\t\t// Compute paths/values\n\t\tconst string uri = fileUriFor(getValueString(\"sync_dir\")); \n\t\tconst string xbelPath = buildPath(expandTilde(environment.get(\"HOME\", \"\")), \".local\", \"share\", \"user-places.xbel\");\n\n\t\tif (!exists(xbelPath)) {\n\t\t\treturn;\n\t\t}\n\n\t\tstring content = readText(xbelPath);\n\t\tauto before = content;\n\n\t\t// Build a regex that matches:\n\t\t// <bookmark ... href=\"URI\" ...> ... </bookmark>\n\t\t// - tolerate attribute order/whitespace\n\t\t// - accept single or double quotes around URI\n\t\t// - non-greedy body match\n\t\tconst esc = regexEscape(uri);\n\t\tauto re = regex(`(?s)<bookmark\\b[^>]*\\bhref\\s*=\\s*[\"']` ~ esc ~ `[\"'][^>]*>.*?</bookmark\\s*>`);\n\n\t\t// Remove all matches\n\t\tcontent = replaceAll(content, re, \"\");\n\n\t\t// Optional: tidy up multiple blank lines left behind\n\t\tauto cleanup = regex(`\\n{3,}`);\n\t\tcontent = replaceAll(content, cleanup, \"\\n\\n\");\n\n\t\t// If nothing changed, exit quietly\n\t\tif (content == before) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Atomic write\n\t\tstring tmp = xbelPath ~ \".tmp\";\n\t\tstd.file.write(tmp, content);\n\t\trename(tmp, xbelPath);\n\n\t\taddLogEntry(\"KDE Desktop Integration: KDE/Plasma place removed successfully\", [\"info\"]);\n\t}\n\t\n\t// Safely merge multiple '|'-delimited rule strings by normalising, removing empty entries, trimming whitespace, \n\t// and de-duplicating rules so malformed or repeated config entries do not corrupt 'skip_dir' / 'skip_file' processing\n\tprivate string mergePipeDelimitedRulesDedup(const string existing, const string incoming) {\n\t\tauto resultString = appender!(string[])();\n\n\t\tvoid addTokens(const string raw) {\n\t\t\tforeach (part; raw.splitter('|')) {\n\t\t\t\tauto t = part.strip();\n\t\t\t\tif (t.empty) continue;\n\t\t\t\t// Keep first occurrence\n\t\t\t\tif (!resultString.data.canFind(t)) resultString ~= t.idup;\n\t\t\t}\n\t\t}\n\n\t\taddTokens(existing);\n\t\taddTokens(incoming);\n\n\t\treturn resultString.data.joiner(\"|\").to!string;\n\t}\n}\n\n// Output the full application help when --help is passed in\nvoid outputLongHelp(Option[] opt) {\n\t\tauto argsNeedingOptions = [\n\t\t\t\"--auth-files\",\n\t\t\t\"--auth-response\",\n\t\t\t\"--confdir\",\n\t\t\t\"--create-directory\",\n\t\t\t\"--classify-as-big-delete\",\n\t\t\t\"--create-share-link\",\n\t\t\t\"--destination-directory\",\n\t\t\t\"--download-file\",\n\t\t\t\"--get-file-link\",\n\t\t\t\"--get-O365-drive-id\",\n\t\t\t\"--get-sharepoint-drive-id\",\n\t\t\t\"--log-dir\",\n\t\t\t\"--min-notify-changes\",\n\t\t\t\"--modified-by\",\n\t\t\t\"--monitor-interval\",\n\t\t\t\"--monitor-log-frequency\",\n\t\t\t\"--monitor-fullscan-frequency\",\n\t\t\t\"--remove-directory\",\n\t\t\t\"--single-directory\",\n\t\t\t\"--skip-dir\",\n\t\t\t\"--skip-file\",\n\t\t\t\"--skip-size\",\n\t\t\t\"--source-directory\",\n\t\t\t\"--space-reservation\",\n\t\t\t\"--syncdir\",\n\t\t\t\"--share-password\",\n\t\t\t\"--user-agent\" ];\n\t\twriteln(`onedrive - A client for the Microsoft OneDrive Cloud Service\n\n  Usage:\n    onedrive [options] --sync\n      Do a one-time synchronisation with Microsoft OneDrive\n    onedrive [options] --monitor\n      Monitor filesystem and synchronise regularly with Microsoft OneDrive\n    onedrive [options] --display-config\n      Display the currently used configuration\n    onedrive [options] --display-sync-status\n      Query OneDrive service and report on pending changes\n    onedrive -h | --help\n      Show this help screen\n    onedrive --version\n      Show version\n\n  Options:\n\t`);\n\t\tforeach (it; opt.sort!(\"a.optLong < b.optLong\")) {\n\t\t\twritefln(\"  %s%s%s%s\\n      %s\",\n\t\t\t\t\tit.optLong,\n\t\t\t\t\tit.optShort == \"\" ? \"\" : \" \" ~ it.optShort,\n\t\t\t\t\targsNeedingOptions.canFind(it.optLong) ? \" '<path or required value>'\" : \"\",\n\t\t\t\t\tit.required ? \" (required)\" : \"\", it.help);\n\t\t}\n\t\t// end with a blank line\n\t\twriteln();\n}\n"
  },
  {
    "path": "src/curlEngine.d",
    "content": "// What is this module called?\nmodule curlEngine;\n\n// What does this module require to function?\nimport std.net.curl;\nimport etc.c.curl;\nimport std.datetime;\nimport std.conv;\nimport std.file;\nimport std.format;\nimport std.json;\nimport std.stdio;\nimport std.range;\nimport core.memory;\nimport core.sys.posix.signal;\n// Required for WebSocket Support\nimport core.stdc.stdlib : getenv;\nimport core.stdc.string : strcmp;\nimport core.sys.posix.dlfcn : dlopen, dlsym, dlclose, RTLD_NOW; // Posix elements\nimport std.exception : enforce;     // for enforce(...)\n\n// What other modules that we have created do we need to import?\nimport log;\nimport util;\n\n// WebSocket check elements\nenum CURL_WS_MIN_NUM = 0x075600; // 7.86.0 (version which WebSocket support was added to cURL)\n\nextern (C) void sigpipeHandler(int signum) {\n\t// Custom handler to ignore SIGPIPE signals\n\taddLogEntry(\"ERROR: Handling a cURL SIGPIPE signal despite CURLOPT_NOSIGNAL being set (cURL Operational Bug) ...\");\n}\n\n// Function pointer types matching libcurl WebSocket (WS) API\nextern(C) struct curl_ws_frame {\n\tuint age;\n\tuint flags;\n\tsize_t len;\n\tsize_t offset;\n\tsize_t bytesleft;\n}\n\n// WebSocket alias\nalias PFN_curl_ws_recv =\n\textern(C) CURLcode function(CURL*, void*, size_t, size_t*, const curl_ws_frame**);\nalias PFN_curl_ws_send =\n\textern(C) CURLcode function(CURL*, const void*, size_t, size_t*, long /*curl_off_t*/, uint);\n\nextern(C) struct curl_slist { char* data; curl_slist* next; }\nextern(C) curl_slist* curl_slist_append(curl_slist* list, const char* string);\nextern(C) void curl_slist_free_all(curl_slist* list);\n\n// Shared pool of CurlEngine instances accessible across all threads\n__gshared CurlEngine[] curlEnginePool; // __gshared is used to declare a variable that is shared across all threads\n\nprivate __gshared {\n\tvoid*                 _curlLib;\n\tPFN_curl_ws_recv      p_curl_ws_recv;\n\tPFN_curl_ws_send      p_curl_ws_send;\n\tbool                  _wsSymbolsReady;\n\tuint                  _wsProbeOnce; // 0=not run, 1=success, 2=fail\n}\n\nprivate void* loadCurlLib() {\n\t// Respect LD_LIBRARY_PATH etc.\n\tauto h = dlopen(\"libcurl.so.4\", RTLD_NOW);\n\tif (h is null) h = dlopen(\"libcurl.so\", RTLD_NOW);\n\treturn h;\n}\n\nprivate void* findSymbol(const(char)* name) {\n\treturn dlsym(_curlLib, name);\n}\n\nprivate bool probeCurlWsSymbols() {\n\tif (_wsProbeOnce == 1) return _wsSymbolsReady;\n\tif (_wsProbeOnce == 2) return false;\n\n\t// 1) libcurl version check\n\tauto vi = curl_version_info(CURLVERSION_NOW);\n\tif (vi is null || vi.version_num < CURL_WS_MIN_NUM) {\n\t\t_wsProbeOnce = 2; _wsSymbolsReady = false; return false;\n\t}\n\n\t// 2) load libcurl and resolve symbols\n\t_curlLib = loadCurlLib();\n\tif (_curlLib is null) {\n\t\t_wsProbeOnce = 2; _wsSymbolsReady = false; return false;\n\t}\n\n\tp_curl_ws_recv = cast(PFN_curl_ws_recv) findSymbol(\"curl_ws_recv\");\n\tp_curl_ws_send = cast(PFN_curl_ws_send) findSymbol(\"curl_ws_send\");\n\n\t_wsSymbolsReady = (p_curl_ws_recv !is null) && (p_curl_ws_send !is null);\n\t_wsProbeOnce = _wsSymbolsReady ? 1 : 2;\n\treturn _wsSymbolsReady;\n}\n\nbool curlSupportsWebSockets() {\n\treturn probeCurlWsSymbols();\n}\n\nclass CurlResponse {\n\tHTTP.Method method;\n\tconst(char)[] url;\n\tconst(char)[][const(char)[]] requestHeaders;\n\tconst(char)[] postBody;\n\n\tbool hasResponse;\n\tstring[string] responseHeaders;\n\tHTTP.StatusLine statusLine;\n\tchar[] content;\n\n\tthis() {\n\t\treset();\n\t}\n\t\n\t~this() {\n\t\treset();\n\t}\n\n\tvoid reset() {\n\t\tmethod = HTTP.Method.undefined;\n\t\turl = \"\";\n\t\trequestHeaders = null;\n\t\tpostBody = [];\n\t\thasResponse = false;\n\t\tresponseHeaders = null;\n\t\tstatusLine.reset();\n\t\tcontent = [];\n\t}\n\n\tvoid addRequestHeader(const(char)[] name, const(char)[] value) {\n\t\trequestHeaders[to!string(name)] = to!string(value);\n\t}\n\n\tvoid connect(HTTP.Method method, const(char)[] url) {\n\t\tthis.method = method;\n\t\tthis.url = url;\n\t}\n\n\tconst JSONValue json() {\n\t\tJSONValue json;\n\t\ttry {\n\t\t\tjson = content.parseJSON();\n\t\t} catch (JSONException e) {\n\t\t\t// Log that a JSON Exception was caught, dont output the HTML response from OneDrive\n\t\t\tif (debugLogging) {addLogEntry(\"JSON Exception caught when performing HTTP operations - use --debug-https to diagnose further\", [\"debug\"]);}\n\t\t}\n\t\treturn json;\n\t};\n\n\tvoid update(HTTP *http) {\n\t\thasResponse = true;\n\t\tthis.responseHeaders = http.responseHeaders();\n\t\tthis.statusLine = http.statusLine;\n\t\t\n\t\t// has 'microsoftDataCentre' been set yet?\n\t\tif (microsoftDataCentre.empty) {\n\t\t\t// Extract the 'x-ms-ags-diagnostic' header if it exists\n\t\t\tif (\"x-ms-ags-diagnostic\" in this.responseHeaders) {\n\t\t\t\t// try and extract the data centre details\n\t\t\t\ttry {\n\t\t\t\t\t// attempt to extract the data centre location from the header\n\t\t\t\t\tauto diagHeaderData = parseJSON(this.responseHeaders[\"x-ms-ags-diagnostic\"]);\n\t\t\t\t\tstring dataCentre = diagHeaderData[\"ServerInfo\"][\"DataCenter\"].str;\n\t\t\t\t\t// set the Microsoft Data Centre value\n\t\t\t\t\tmicrosoftDataCentre = dataCentre;\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t// do nothing\n\t\t\t\t}\t\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t\t// Output the response headers only if using debug mode + debugging https itself\n\t\tif ((debugLogging) && (debugHTTPSResponse)) {\n\t\t\taddLogEntry(\"HTTP Response Headers: \" ~ to!string(this.responseHeaders), [\"debug\"]);\n\t\t\taddLogEntry(\"HTTP Status Line: \" ~ to!string(this.statusLine), [\"debug\"]);\n\t\t}\n\t}\n\n\t@safe pure HTTP.StatusLine getStatus() {\n\t\treturn this.statusLine;\n\t}\n\n\t// Return the current value of retryAfterValue\n\tint getRetryAfterValue() {\n\t\tint delayBeforeRetry;\n\t\t// Is 'retry-after' in the response headers\n\t\tif (\"retry-after\" in responseHeaders) {\n\t\t\t// Set the retry-after value\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"curlEngine.http.perform() => Received a 'Retry-After' Header Response with the following value: \" ~ to!string(responseHeaders[\"retry-after\"]), [\"debug\"]);\n\t\t\t\taddLogEntry(\"curlEngine.http.perform() => Setting retryAfterValue to: \" ~ responseHeaders[\"retry-after\"], [\"debug\"]);\n\t\t\t}\n\t\t\tdelayBeforeRetry = to!int(responseHeaders[\"retry-after\"]);\n\t\t} else {\n\t\t\t// Use a 120 second delay as a default given header value was zero\n\t\t\t// This value is based on log files and data when determining correct process for 429 response handling\n\t\t\tdelayBeforeRetry = 120;\n\t\t\t// Update that we are over-riding the provided value with a default\n\t\t\tif (debugLogging) {addLogEntry(\"HTTP Response Header retry-after value was missing - Using a preconfigured default of: \" ~ to!string(delayBeforeRetry), [\"debug\"]);}\n\t\t}\n\t\treturn delayBeforeRetry;\n\t}\n\t\n\tconst string parseRequestHeaders(const(const(char)[][const(char)[]]) headers) {\n\t\tstring requestHeadersStr = \"\";\n\t\t// Ensure response headers is not null and iterate over keys safely.\n\t\tif (headers !is null) {\n\t\t\tforeach (string header; headers.byKey()) {\n\t\t\t\tif (header == \"Authorization\") {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Use the 'in' operator to safely check if the key exists in the associative array.\n\t\t\t\tif (auto val = header in headers) {\n\t\t\t\t\trequestHeadersStr ~= \"< \" ~ header ~ \": \" ~ *val ~ \"\\n\";\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn requestHeadersStr;\n\t}\n\n\tconst string parseResponseHeaders(const(string[string]) headers) {\n\t\tstring responseHeadersStr = \"\";\n\t\t// Ensure response headers is not null and iterate over keys safely.\n\t\tif (headers !is null) {\n\t\t\tforeach (string header; headers.byKey()) {\n\t\t\t\t// Check if the key actually exists before accessing it to avoid RangeError.\n\t\t\t\tif (auto val = header in headers) { // 'in' checks for the key and returns a pointer to the value if found.\n\t\t\t\t\tresponseHeadersStr ~= \"> \" ~ header ~ \": \" ~ *val ~ \"\\n\"; // Dereference pointer to get the value.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn responseHeadersStr;\n\t}\n\n\tconst string dumpDebug() {\n\t\timport std.range;\n\t\timport std.format : format;\n\t\t\n\t\tstring str = \"\";\n\t\tstr ~= format(\"< %s %s\\n\", method, url);\n\t\tif (!requestHeaders.empty) {\n\t\t\tstr ~= parseRequestHeaders(requestHeaders);\n\t\t}\n\t\tif (!postBody.empty) {\n\t\t\tstr ~= format(\"\\n----\\n%s\\n----\\n\", postBody);\n\t\t}\n\t\tstr ~= format(\"< %s\\n\", statusLine);\n\t\tif (!responseHeaders.empty) {\n\t\t\tstr ~= parseResponseHeaders(responseHeaders);\n\t\t}\n\t\treturn str;\n\t}\n\n\tconst string dumpResponse() {\n\t\timport std.range;\n\t\timport std.format : format;\n\n\t\tstring str = \"\";\n\t\tif (!content.empty) {\n\t\t\tstr ~= format(\"\\n----\\n%s\\n----\\n\", content);\n\t\t}\n\t\treturn str;\n\t}\n\n\toverride string toString() const {\n\t\tstring str = \"Curl debugging: \\n\";\n\t\tstr ~= dumpDebug();\n\t\tif (hasResponse) {\n\t\t\tstr ~= \"Curl response: \\n\";\n\t\t\tstr ~= dumpResponse();\n\t\t}\n\t\treturn str;\n\t}\n}\n\nclass CurlEngine {\n\n\tHTTP http;\n\tFile uploadFile;\n\tCurlResponse response;\n\tbool keepAlive;\n\tulong dnsTimeout;\n\tstring internalThreadId;\n\tSysTime releaseTimestamp;\n\tulong maxIdleTime;\n\tprivate long resumeFromOffset = -1;\n\t\n    this() {\n        http = HTTP();   // Directly initializes HTTP using its default constructor\n        response = null; // Initialize as null\n\t\tinternalThreadId = generateAlphanumericString(); // Give this CurlEngine instance a unique ID\n\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"Created new CurlEngine instance id: \" ~ to!string(internalThreadId), [\"debug\"]);}\n    }\n\n\t// The destructor should only clean up resources owned directly by this CurlEngine instance\n\t~this() {\n\t\t// Is the file still open?\n\t\tif (uploadFile.isOpen()) {\n\t\t\tuploadFile.close();\n\t\t}\n\t\t// Is 'response' cleared?\n\t\tobject.destroy(response); // Destroy, then set to null\n\t\tresponse = null;\n\t\t// Is the actual http instance is stopped?\n\t\tif (!http.isStopped) {\n\t\t\thttp.shutdown();\n\t\t}\n\t\t// Make sure this HTTP instance is destroyed\n\t\tobject.destroy(http);\n\t\t// ThreadId needs to be set to null\n\t\tinternalThreadId = null;\n    }\n\t\t\n\t// We are releasing a curl instance back to the pool\n\tvoid releaseEngine() {\n\t\t// Set timestamp of release\n\t\treleaseTimestamp = Clock.currTime(UTC());\n\t\t// Log that we are releasing this engine back to the pool\n\t\tif ((debugLogging) && (debugHTTPSResponse)) {\n\t\t\taddLogEntry(\"CurlEngine releaseEngine() called on instance id: \" ~ to!string(internalThreadId), [\"debug\"]);\n\t\t\taddLogEntry(\"CurlEngine curlEnginePool size before release: \" ~ to!string(curlEnginePool.length), [\"debug\"]);\n\t\t\tstring engineReleaseMessage = format(\"Release Timestamp for CurlEngine %s: %s\", to!string(internalThreadId), to!string(releaseTimestamp));\n\t\t\taddLogEntry(engineReleaseMessage, [\"debug\"]);\n\t\t}\n\t\t\n\t\t// cleanup this curl instance before putting it back in the pool\n\t\tcleanup(true); // Cleanup instance by resetting values and flushing cookie cache\n        synchronized (CurlEngine.classinfo) {\n            curlEnginePool ~= this;\n\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine curlEnginePool size after release: \" ~ to!string(curlEnginePool.length), [\"debug\"]);}\n        }\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t// Return free memory to the OS\n\t\tGC.minimize();\n    }\n\t\n\t// Setup a specific SIGPIPE Signal handler due to curl bugs that ignore CurlOption.nosignal\n\tvoid setupSIGPIPESignalHandler() {\n\t\t// Setup the signal handler\n\t\tsigaction_t curlAction;\n\t\tcurlAction.sa_handler = &sigpipeHandler; // Direct function pointer assignment\n\t\tsigaction(SIGPIPE, &curlAction, null); // Broken Pipe signal from curl\n\t}\n\t\n\t// Initialise this curl instance\n\tvoid 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) {\n\t\t// There are many broken curl versions being used, mainly provided by Ubuntu\n\t\t// 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\n\t\tsetupSIGPIPESignalHandler();\n\t\t\n\t\t// Setting 'keepAlive' to false ensures that when we close the curl instance, any open sockets are closed - which we need to do when running \n\t\t// multiple threads and API instances at the same time otherwise we run out of local files | sockets pretty quickly\n\t\tthis.keepAlive = keepAlive;\n\t\t\n\t\t// Curl DNS Timeout Handling\n\t\tthis.dnsTimeout = dnsTimeout;\n\n\t\t// Curl Timeout Handling\n\t\tthis.maxIdleTime = maxIdleTime;\n\t\t\n\t\t// libcurl dns_cache_timeout timeout\n\t\t// https://curl.se/libcurl/c/CURLOPT_DNS_CACHE_TIMEOUT.html\n\t\t// https://dlang.org/library/std/net/curl/http.dns_timeout.html\n\t\thttp.dnsTimeout = (dur!\"seconds\"(dnsTimeout));\n\t\t\n\t\t// Timeout for HTTPS connections\n\t\t// https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html\n\t\t// https://dlang.org/library/std/net/curl/http.connect_timeout.html\n\t\thttp.connectTimeout = (dur!\"seconds\"(connectTimeout));\n\t\t\n\t\t// Timeout for activity on connection\n\t\t// This is a DMD | DLANG specific item, not a libcurl item\n\t\t// https://dlang.org/library/std/net/curl/http.data_timeout.html\n\t\t// https://raw.githubusercontent.com/dlang/phobos/master/std/net/curl.d - private enum _defaultDataTimeout = dur!\"minutes\"(2);\n\t\thttp.dataTimeout = (dur!\"seconds\"(dataTimeout));\n\t\t\n\t\t// Maximum time any operation is allowed to take\n\t\t// This includes dns resolution, connecting, data transfer, etc.\n\t\t// https://curl.se/libcurl/c/CURLOPT_TIMEOUT_MS.html\n\t\t// https://dlang.org/library/std/net/curl/http.operation_timeout.html\n\t\thttp.operationTimeout = (dur!\"seconds\"(operationTimeout));\n\t\t\n\t\t// Specify how many redirects should be allowed\n\t\thttp.maxRedirects(maxRedirects);\n\t\t// Debug HTTPS\n\t\thttp.verbose = httpsDebug;\n\t\t// Use the configured 'user_agent' value\n\t\thttp.setUserAgent = userAgent;\n\t\t// What IP protocol version should be used when using Curl - IPv4 & IPv6, IPv4 or IPv6\n\t\thttp.handle.set(CurlOption.ipresolve,protocolVersion); // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only\n\t\t\n\t\t// What version of HTTP protocol do we use?\n\t\t// Curl >= 7.62.0 defaults to http2 for a significant number of operations\n\t\tif (httpProtocol) {\n\t\t\t// Downgrade to HTTP 1.1 - yes version = 2 is HTTP 1.1\n\t\t\thttp.handle.set(CurlOption.http_version,2);\n\t\t}\n\t\t\n\t\t// Configure upload / download rate limits if configured\n\t\t// 131072 = 128 KB/s - minimum for basic application operations to prevent timeouts\n\t\t// A 0 value means rate is unlimited, and is the curl default\n\t\tif (userRateLimit > 0) {\n\t\t\t// set rate limit\n\t\t\thttp.handle.set(CurlOption.max_send_speed_large,userRateLimit);\n\t\t\thttp.handle.set(CurlOption.max_recv_speed_large,userRateLimit);\n\t\t}\n\t\t\n\t\t// Explicitly set libcurl options to avoid using signal handlers in a multi-threaded environment\n\t\t// See: https://curl.se/libcurl/c/CURLOPT_NOSIGNAL.html\n\t\t// The CURLOPT_NOSIGNAL option is intended for use in multi-threaded programs to ensure that libcurl does not use any signal handling.\n\t\t// 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.\n\t\thttp.handle.set(CurlOption.nosignal,1);\n\t\t\n\t\t//   https://curl.se/libcurl/c/CURLOPT_TCP_NODELAY.html\n\t\t//   Ensure that TCP_NODELAY is set to 0 to ensure that TCP NAGLE is enabled\n\t\thttp.handle.set(CurlOption.tcp_nodelay,0);\n\t\t\n\t\t//   https://curl.se/libcurl/c/CURLOPT_FORBID_REUSE.html\n\t\t//   CURLOPT_FORBID_REUSE - make connection get closed at once after use\n\t\t//   Setting this to 0 ensures that we ARE reusing connections (we did this in v2.4.xx) to ensure connections remained open and usable\n\t\t//   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\n\t\t//   The libcurl default is 0 as per the documentation (to REUSE connections) - ensure we are configuring to reuse sockets\n\t\thttp.handle.set(CurlOption.forbid_reuse,0);\n\t\t\n\t\tif (httpsDebug) {\n\t\t\t// Output what options we are using so that in the debug log this can be tracked\n\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {\n\t\t\t\taddLogEntry(\"http.dnsTimeout = \" ~ to!string(dnsTimeout), [\"debug\"]);\n\t\t\t\taddLogEntry(\"http.connectTimeout = \" ~ to!string(connectTimeout), [\"debug\"]);\n\t\t\t\taddLogEntry(\"http.dataTimeout = \" ~ to!string(dataTimeout), [\"debug\"]);\n\t\t\t\taddLogEntry(\"http.operationTimeout = \" ~ to!string(operationTimeout), [\"debug\"]);\n\t\t\t\taddLogEntry(\"http.maxRedirects = \" ~ to!string(maxRedirects), [\"debug\"]);\n\t\t\t\taddLogEntry(\"http.CurlOption.ipresolve = \" ~ to!string(protocolVersion), [\"debug\"]);\n\t\t\t\taddLogEntry(\"http.header.Connection.keepAlive = \" ~ to!string(keepAlive), [\"debug\"]);\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid setResponseHolder(CurlResponse response) {\n\t\tif (response is null) {\n\t\t\t// Create a response instance if it doesn't already exist\n\t\t\tif (this.response is null)\n\t\t\t\tthis.response = new CurlResponse();\n\t\t} else {\n\t\t\tthis.response = response;\n\t\t}\n\t}\n\n\tvoid addRequestHeader(const(char)[] name, const(char)[] value) {\n\t\tsetResponseHolder(null);\n\t\thttp.addRequestHeader(name, value);\n\t\tresponse.addRequestHeader(name, value);\n\t}\n\n\tvoid connect(HTTP.Method method, const(char)[] url) {\n\t\tsetResponseHolder(null);\n\t\tif (!keepAlive)\n\t\t\taddRequestHeader(\"Connection\", \"close\");\n\t\thttp.method = method;\n\t\thttp.url = url;\n\t\tresponse.connect(method, url);\n\t}\n\n\tvoid setContent(const(char)[] contentType, const(char)[] sendData) {\n\t\tsetResponseHolder(null);\n\t\taddRequestHeader(\"Content-Type\", contentType);\n\t\tif (sendData) {\n\t\t\thttp.contentLength = sendData.length;\n\t\t\thttp.onSend = (void[] buf) {\n\t\t\t\timport std.algorithm: min;\n\t\t\t\tsize_t minLen = min(buf.length, sendData.length);\n\t\t\t\tif (minLen == 0) return 0;\n\t\t\t\tbuf[0 .. minLen] = cast(void[]) sendData[0 .. minLen];\n\t\t\t\tsendData = sendData[minLen .. $];\n\t\t\t\treturn minLen;\n\t\t\t};\n\t\t\tresponse.postBody = sendData;\n\t\t}\n\t}\n\n\tvoid setFile(string filepath, string contentRange, ulong offset, ulong offsetSize) {\n\t\tsetResponseHolder(null);\n\t\t// open file as read-only in binary mode\n\t\tuploadFile = File(filepath, \"rb\");\n\n\t\tif (contentRange.empty) {\n\t\t\toffsetSize = uploadFile.size();\n\t\t} else {\n\t\t\taddRequestHeader(\"Content-Range\", contentRange);\n\t\t\tuploadFile.seek(offset);\n\t\t}\n\n\t\t// Setup progress bar to display\n\t\thttp.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) {\n\t\t\treturn 0;\n\t\t};\n\t\t\n\t\taddRequestHeader(\"Content-Type\", \"application/octet-stream\");\n\t\thttp.onSend = data => uploadFile.rawRead(data).length;\n\t\thttp.contentLength = offsetSize;\n\t}\n\t\n\tvoid setZeroContentLength() {\n\t\t// Explicit HTTP semantics\n\t\thttp.contentLength = 0;\n\t\taddRequestHeader(\"Content-Length\", to!string(0));\n\t\t\n\t\t// Force libcurl POST-with-empty-body semantics\n\t\t// This prevents libcurl from attempting to read from stdin when performing a POST with no payload.\n\t\thttp.handle.set(CurlOption.postfields, \"\");\n\t\thttp.handle.set(CurlOption.postfieldsize, 0L);\n\t\t\n\t\t// Defensive: ensure we are NOT in upload/read-callback mode\n\t\thttp.handle.set(CurlOption.upload, 0);\n\t}\n\n\tCurlResponse execute() {\n\t\tscope(exit) {\n\t\t\tcleanup();\n\t\t}\n\t\tsetResponseHolder(null);\n\t\thttp.onReceive = (ubyte[] data) {\n\t\t\tresponse.content ~= data;\n\t\t\t// HTTP Server Response Code Debugging if --https-debug is being used\n\t\t\treturn data.length;\n\t\t};\n\t\thttp.perform();\n\t\tresponse.update(&http);\n\t\treturn response;\n\t}\n\n\tCurlResponse download(string originalFilename, string downloadFilename) {\n\t\tsetResponseHolder(null);\n\t\t\n\t\t// Open the file in append mode if resuming, else write mode\n\t\tauto file = (resumeFromOffset > 0)\n\t\t\t? File(downloadFilename, \"ab\") // append binary\n\t\t\t: File(downloadFilename, \"wb\"); // write binary\n\n\t\t// Function exit scope\n\t\tscope(exit) {\n\t\t\tcleanup();\n\t\t\tif (file.isOpen()){\n\t\t\t\t// close open file\n\t\t\t\tfile.close();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Apply Range header if resuming\n\t\tif (resumeFromOffset > 0) {\n\t\t\tstring rangeHeader = format(\"bytes=%d-\", resumeFromOffset);\n\t\t\taddRequestHeader(\"Range\", rangeHeader);\n\t\t}\n\t\t\n\t\t// Receive data\n\t\thttp.onReceive = (ubyte[] data) {\n\t\t\tfile.rawWrite(data);\n\t\t\treturn data.length;\n\t\t};\n\t\t\n\t\t// Perform HTTP Operation\n\t\thttp.perform();\n\t\t\n\t\t// close open file - avoids problems with renaming on GCS Buckets and other semi-POSIX systems\n\t\tif (file.isOpen()){\n\t\t\tfile.close();\n\t\t}\n\t\t\n\t\t// Rename downloaded file\n\t\trename(downloadFilename, originalFilename);\n\n\t\t// Update response and return response\n\t\tresponse.update(&http);\n\t\treturn response;\n\t}\n\n\t// Cleanup this instance internal variables that may have been set\n\tvoid cleanup(bool flushCookies = false) {\n\t\t// Reset any values to defaults, freeing any set objects\n\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine cleanup() called on instance id: \" ~ to!string(internalThreadId), [\"debug\"]);}\n\t\t\n\t\t// Is the instance is stopped?\n\t\tif (!http.isStopped) {\n\t\t\t// A stopped instance is not usable, these cannot be reset\n\t\t\thttp.clearRequestHeaders();\n\t\t\thttp.onSend = null;\n\t\t\thttp.onReceive = null;\n\t\t\thttp.onReceiveHeader = null;\n\t\t\thttp.onReceiveStatusLine = null;\n\t\t\thttp.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) {\n\t\t\t\treturn 0;\n\t\t\t};\n\t\t\thttp.contentLength = 0;\n\t\t\t\n\t\t\t// We only do this if we are pushing the curl engine back to the curl pool\n\t\t\tif (flushCookies) {\n\t\t\t\t// Flush the cookie cache as well\n\t\t\t\thttp.flushCookieJar();\n\t\t\t\thttp.clearSessionCookies();\n\t\t\t\thttp.clearAllCookies();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// set the response to null\n\t\tresponse = null;\n\n\t\t// close file if open\n\t\tif (uploadFile.isOpen()){\n\t\t\t// close open file\n\t\t\tuploadFile.close();\n\t\t}\n\t}\n\n\t// Shut down the curl instance & close any open sockets\n\tvoid shutdownCurlHTTPInstance() {\n\t\t// Log that we are attempting to shutdown this curl instance\n\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine shutdownCurlHTTPInstance() called on instance id: \" ~ to!string(internalThreadId), [\"debug\"]);}\n\t\t\n\t\t// Is this curl instance is stopped?\n\t\tif (!http.isStopped) {\n\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {\n\t\t\t\taddLogEntry(\"HTTP instance still active: \" ~ to!string(internalThreadId), [\"debug\"]);\n\t\t\t\taddLogEntry(\"HTTP instance isStopped state before http.shutdown(): \" ~ to!string(http.isStopped), [\"debug\"]);\n\t\t\t}\n\t\t\thttp.shutdown();\n\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"HTTP instance isStopped state post http.shutdown(): \" ~ to!string(http.isStopped), [\"debug\"]);}\n\t\t\tobject.destroy(http); // Destroy, however we cant set to null\n\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"HTTP instance shutdown and destroyed: \" ~ to!string(internalThreadId), [\"debug\"]);}\n\t\t\t\n\t\t} else {\n\t\t\t// Already stopped .. destroy it\n\t\t\tobject.destroy(http); // Destroy, however we cant set to null\n\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"Stopped HTTP instance shutdown and destroyed: \" ~ to!string(internalThreadId), [\"debug\"]);}\n\t\t}\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t// Return free memory to the OS\n\t\tGC.minimize();\n\t}\n\t\n\t// Disable SSL certificate peer verification for libcurl operations.\n\t//\n\t// This function disables the verification of the SSL peer's certificate\n\t// by setting CURLOPT_SSL_VERIFYPEER to 0. This means that libcurl will\n\t// accept any certificate presented by the server, regardless of whether\n\t// it is signed by a trusted certificate authority.\n\t//\n\t// -------------------------------------------------------------------------------------\n\t// WARNING: Disabling SSL peer verification introduces significant security risks:\n\t// -------------------------------------------------------------------------------------\n\t// - Man-in-the-Middle (MITM) attacks become trivially possible.\n\t// - Malicious servers can impersonate trusted endpoints.\n\t// - Confidential data (authentication tokens, file contents) can be intercepted.\n\t// - Violates industry security standards and regulatory compliance requirements.\n\t// - Should never be used in production environments or on untrusted networks.\n\t//\n\t// This option should only be enabled for internal testing, debugging self-signed\n\t// certificates, or explicitly controlled environments with known risks.\n\t//\n\t// See also:\n\t// https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html\n\tvoid setDisableSSLVerifyPeer() {\n\t\t// Emit a runtime warning if debug logging is enabled\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"WARNING: SSL peer verification has been DISABLED!\", [\"debug\"]);\n\t\t\taddLogEntry(\"         This allows invalid or self-signed certificates to be accepted.\", [\"debug\"]);\n\t\t\taddLogEntry(\"         Use ONLY for testing. This severely weakens HTTPS security.\", [\"debug\"]);\n\t\t}\n\n\t\t// Disable SSL certificate verification (DANGEROUS)\n\t\thttp.handle.set(CurlOption.ssl_verifypeer, 0);\n\t}\n\t\n\t// Enable SSL Certificate Verification\n\tvoid setEnableSSLVerifyPeer() {\n\t\t// Enable SSL certificate verification\n\t\taddLogEntry(\"Enabling SSL peer verification\");\n\t\thttp.handle.set(CurlOption.ssl_verifypeer, 1);\n\t}\n\t\n\t// Set an applicable resumable offset point when downloading a file\n\tvoid setDownloadResumeOffset(long offset) {\n\t\tresumeFromOffset = offset;\n\t}\n\t\n\t// reset resumable offset point to negative value\n\tvoid resetDownloadResumeOffset() {\n\t\tresumeFromOffset = -1;\n\t}\n}\n\n// Methods to control obtaining and releasing a CurlEngine instance from the curlEnginePool\n\n// Get a curl instance for the OneDrive API to use\nCurlEngine getCurlInstance() {\n\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine getCurlInstance() called\", [\"debug\"]);}\n\t\n\tsynchronized (CurlEngine.classinfo) {\n\t\t// What is the current pool size\n\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine curlEnginePool current size: \" ~ to!string(curlEnginePool.length), [\"debug\"]);}\n\t\n\t\tif (curlEnginePool.empty) {\n\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine curlEnginePool is empty - constructing a new CurlEngine instance\", [\"debug\"]);}\n\t\t\treturn new CurlEngine;  // Constructs a new CurlEngine with a fresh HTTP instance\n\t\t} else {\n\t\t\tCurlEngine curlEngine = curlEnginePool[$ - 1];\n\t\t\tcurlEnginePool.popBack(); // assumes a LIFO (last-in, first-out) usage pattern\n\t\t\t\n\t\t\t// Is this engine stopped?\n\t\t\tif (curlEngine.http.isStopped) {\n\t\t\t\t// return a new curl engine as a stopped one cannot be used\n\t\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine was in a stopped state (not usable) - constructing a new CurlEngine instance\", [\"debug\"]);}\n\t\t\t\treturn new CurlEngine;  // Constructs a new CurlEngine with a fresh HTTP instance\n\t\t\t} else {\n\t\t\t\t// When was this engine last used?\n\t\t\t\tauto elapsedTime = Clock.currTime(UTC()) - curlEngine.releaseTimestamp;\n\t\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {\n\t\t\t\t\tstring engineIdleMessage = format(\"CurlEngine %s time since last use: %s\", to!string(curlEngine.internalThreadId), to!string(elapsedTime));\n\t\t\t\t\taddLogEntry(engineIdleMessage, [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If greater than 120 seconds (default), the treat this as a stale engine, preventing:\n\t\t\t\t// \t* Too old connection (xxx seconds idle), disconnect it\n\t\t\t\t// \t* Connection 0 seems to be dead!\n\t\t\t\t// \t* Closing connection 0\n\t\t\t\t\n\t\t\t\tif (elapsedTime > dur!\"seconds\"(curlEngine.maxIdleTime)) {\n\t\t\t\t\t// Too long idle engine, clean it up and create a new one\n\t\t\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {\n\t\t\t\t\t\tstring curlTooOldMessage = format(\"CurlEngine idle for > %d seconds .... destroying and returning a new curl engine instance\", curlEngine.maxIdleTime);\n\t\t\t\t\t\taddLogEntry(curlTooOldMessage, [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tcurlEngine.cleanup(true); // Cleanup instance by resetting values and flushing cookie cache\n\t\t\t\t\tcurlEngine.shutdownCurlHTTPInstance();  // Assume proper cleanup of any resources used by HTTP\n\t\t\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"Returning NEW curlEngine instance\", [\"debug\"]);}\n\t\t\t\t\treturn new CurlEngine;  // Constructs a new CurlEngine with a fresh HTTP instance\n\t\t\t\t} else {\n\t\t\t\t\t// return an existing curl engine\n\t\t\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {\n\t\t\t\t\t\taddLogEntry(\"CurlEngine was in a valid state - returning existing CurlEngine instance\", [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"Using CurlEngine instance ID: \" ~ curlEngine.internalThreadId, [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t\t// return the existing engine\n\t\t\t\t\treturn curlEngine;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Release all CurlEngine instances\nvoid releaseAllCurlInstances() {\n\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine releaseAllCurlInstances() called\", [\"debug\"]);}\n\tsynchronized (CurlEngine.classinfo) {\n\t\t// What is the current pool size\n\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine curlEnginePool size to release: \" ~ to!string(curlEnginePool.length), [\"debug\"]);}\n\t\tif (curlEnginePool.length > 0) {\n\t\t\t// Safely iterate and clean up each CurlEngine instance\n\t\t\tforeach (curlEngineInstance; curlEnginePool) {\n\t\t\t\ttry {\n\t\t\t\t\tcurlEngineInstance.cleanup(true); // Cleanup instance by resetting values and flushing cookie cache\n\t\t\t\t\tcurlEngineInstance.shutdownCurlHTTPInstance();  // Assume proper cleanup of any resources used by HTTP\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t// Log the error or handle it appropriately\n\t\t\t\t\t// e.g., writeln(\"Error during cleanup/shutdown: \", e.toString());\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// It's safe to destroy the object here assuming no other references exist\n\t\t\t\tobject.destroy(curlEngineInstance); // Destroy, then set to null\n\t\t\t\tcurlEngineInstance = null;\n\t\t\t\t// Perform Garbage Collection on this destroyed curl engine\n\t\t\t\tGC.collect();\n\t\t\t\t// Log release\n\t\t\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine destroyed\", [\"debug\"]);}\n\t\t\t}\n\t\t\n\t\t\t// Clear the array after all instances have been handled\n\t\t\tcurlEnginePool.length = 0; // More explicit than curlEnginePool = [];\n\t\t}\n\t}\n\t// Perform Garbage Collection on the destroyed curl engines\n\tGC.collect();\n\t// Return free memory to the OS\n\tGC.minimize();\n\t// Log that all curl engines have been released\n\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"CurlEngine releaseAllCurlInstances() completed\", [\"debug\"]);}\n}\n\n// Return how many curl engines there are\nulong curlEnginePoolLength() {\n\treturn curlEnginePool.length;\n}"
  },
  {
    "path": "src/curlWebsockets.d",
    "content": "// What is this module called?\nmodule curlWebsockets;\n\n/******************************************************************************\n * Minimal RFC6455 WebSocket client over libcurl (CONNECT_ONLY).\n ******************************************************************************/\n\n// What does this module require to function?\nimport etc.c.curl : CURL, CURLcode, curl_easy_cleanup, curl_easy_getinfo,\n\tcurl_easy_init, curl_easy_perform, curl_easy_recv, curl_easy_reset,\n\tcurl_easy_send, curl_easy_setopt;\n\nimport core.stdc.string : memcpy, memmove;\nimport core.time        : MonoTime, dur;\nimport std.array        : Appender, appender;\nimport std.base64       : Base64;\nimport std.meta         : AliasSeq;\nimport std.random       : Random, unpredictableSeed, uniform;\nimport std.range        : empty;\nimport std.string       : indexOf, startsWith, toLower, toStringz;\nimport std.exception    : collectException;\nimport std.conv;\n\n// What other modules that we have created do we need to import?\nimport log;\n\n// ========== Logging Shim ==========\nprivate void logCurlWebsocketOutput(string s) {\n\tif (debugLogging) {\n\t\taddLogEntry(\"WEBSOCKET: \" ~ s, [\"debug\"]);\n\t}\n}\n\nprivate struct WsFrame {\n\tubyte fin;\n\tubyte opcode;\n\tbool  masked;\n\tulong payloadLen;\n\tubyte[4] maskKey;\n\tubyte[] payload;\n}\n\nfinal class CurlWebSocket {\n\nprivate:\n\t// libcurl constants defined locally\n\tenum int  CURLOPT_URL               = 10002;\n\tenum int  CURLOPT_FOLLOWLOCATION    = 52;\n\tenum int  CURLOPT_NOSIGNAL          = 99;\n\tenum int  CURLOPT_USERAGENT         = 10018;\n\tenum int  CURLOPT_SSL_VERIFYPEER    = 64;\n\tenum int  CURLOPT_SSL_VERIFYHOST    = 81;\n\tenum int  CURLOPT_CONNECT_ONLY      = 141;\n\tenum int  CURLOPT_TIMEOUT_MS        = 155;\n\tenum int  CURLOPT_CONNECTTIMEOUT_MS = 156;\n\tenum int  CURLOPT_VERBOSE           = 41;\n\t\n\t// Additional constants needed for WebSocket handling\n\tenum int  CURLOPT_HTTP_VERSION      = 84;   // CURLOPT_HTTP_VERSION\n\tenum int  CURLOPT_SSL_ENABLE_ALPN   = 226;  // CURLOPT_SSL_ENABLE_ALPN\n\tenum int  CURLOPT_SSL_ENABLE_NPN    = 225;  // CURLOPT_SSL_ENABLE_NPN\n\n\t// HTTP version flags (for CURLOPT_HTTP_VERSION)\n\tenum long CURL_HTTP_VERSION_NONE           = 0;\n\tenum long CURL_HTTP_VERSION_1_0            = 1;\n\tenum long CURL_HTTP_VERSION_1_1            = 2;\n\tenum long CURL_HTTP_VERSION_2_0            = 3;\n\tenum long CURL_HTTP_VERSION_2TLS           = 4;\n\tenum long CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE = 5;\n\tenum long CURL_HTTP_VERSION_3              = 30; // (added in curl 7.66.0+)\n\n\tCURL*   curl = null;\n\tbool    websocketConnected = false;\n\tint     connectTimeoutMs   = 10000;\n\tint     ioTimeoutMs        = 15000;\n\tstring  userAgent          = \"\";\n\tbool    httpsDebug         = false;\n\tstring  scheme;\n\tstring  host;\n\tint     port;\n\tstring  hostPort;\n\tstring  pathQuery;\n\tubyte[] recvBuf;\n\tRandom  rng;\n\npublic:\n\tthis() {\n\t\twebsocketConnected = false;\n\t\tcurl = curl_easy_init();\n\t\trng = Random(unpredictableSeed);\n\t\tlogCurlWebsocketOutput(\"Created a new instance of a CurlWebSocket object accessing libcurl for HTTP operations\");\n\t}\n\n\t~this() {\n\t\t// No logging output in ~this()\n\t\tif (curl !is null) {\n\t\t\tcurl_easy_cleanup(curl);\n\t\t\tcurl = null;\n\t\t}\n\t\twebsocketConnected = false;\n    }\n\n\tbool isConnected() {\n\t\treturn websocketConnected;\n\t}\n\n\tvoid setTimeouts(int connectMs, int rwMs) {\n\t\tconnectTimeoutMs = connectMs;\n\t\tioTimeoutMs = rwMs;\n\t}\n\n\tvoid setUserAgent(string ua) {\n\t\tif (!ua.empty) userAgent = ua;\n\t}\n\n\tvoid setHTTPSDebug(bool httpsDebugInput) {\n\t\thttpsDebug = httpsDebugInput;\n\t}\n\n\tint connect(string wsUrl) {\n\t\tif (curl is null) {\n\t\t\tlogCurlWebsocketOutput(\"libcurl handle not initialised\");\n\t\t\treturn -1;\n\t\t}\n\n\t\tParsedUrl p = parseWsUrl(wsUrl);\n\t\tif (!p.ok) {\n\t\t\tlogCurlWebsocketOutput(\"Invalid WebSocket URL: \" ~ wsUrl);\n\t\t\treturn -2;\n\t\t}\n\t\tscheme    = p.scheme;\n\t\thost      = p.host;\n\t\tport      = p.port;\n\t\thostPort  = p.hostPort;\n\t\tpathQuery = p.pathQuery;\n\n\t\tstring connectUrl = (scheme == \"wss\" ? \"https://\" : \"http://\") ~ hostPort ~ pathQuery;\n\t\t\n\t\t// Reset 'curl' using curl_easy_reset\n\t\tcurl_easy_reset(curl);\n\t\t// Configure required curl options\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_NOSIGNAL,           1L);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_FOLLOWLOCATION,     1L);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_USERAGENT,          userAgent.toStringz);   // NUL-terminated\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_CONNECTTIMEOUT_MS,  cast(long)connectTimeoutMs);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_TIMEOUT_MS,         cast(long)ioTimeoutMs);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_SSL_VERIFYPEER,     1L);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_SSL_VERIFYHOST,     2L);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_CONNECT_ONLY,       1L);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_URL,                connectUrl.toStringz);  // NUL-terminated\n\t\t\n\t\t// Force HTTP/1.1 and disable ALPN/NPN\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_SSL_ENABLE_ALPN,    0L);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_SSL_ENABLE_NPN,     0L);\n\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_HTTP_VERSION,       CURL_HTTP_VERSION_1_1);\n\t\t\t\t\n\t\t// Do we enable HTTPS Debugging?\n\t\tif (httpsDebug) {\n\t\t\t// Enable curl verbosity\n\t\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_VERBOSE,        1L);\n\t\t} else {\n\t\t\t// Disable curl verbosity\n\t\t\tcurl_easy_setopt(curl, cast(int)CURLOPT_VERBOSE,        0L);\n\t\t}\n\n\t\tauto rc = curl_easy_perform(curl);\n\t\tif (rc != 0) {\n\t\t\tlogCurlWebsocketOutput(\"libcurl connect failed\");\n\t\t\treturn -3;\n\t\t}\n\n\t\tauto req = buildUpgradeRequest();\n\t\tif (sendAll(req) != 0) {\n\t\t\tlogCurlWebsocketOutput(\"Failed sending HTTP upgrade request\");\n\t\t\treturn -4;\n\t\t}\n\n\t\t// Read headers until CRLFCRLF, with deadline (don’t treat 0-bytes as EOF).\n\t\tstring hdrs;\n\t\tenum maxHdr = 16 * 1024;\n\t\tauto deadline = MonoTime.currTime + dur!\"seconds\"(10);\n\t\t{\n\t\t\tubyte[4096] tmp;\n\t\t\tsize_t total;\n\t\t\tfor (;;) {\n\t\t\t\tint got = recvSome(tmp[]);\n\t\t\t\tif (got < 0) {\n\t\t\t\t\tlogCurlWebsocketOutput(\"Failed receiving HTTP upgrade response\");\n\t\t\t\t\treturn -5;\n\t\t\t\t}\n\t\t\t\tif (got == 0) {\n\t\t\t\t\tif (MonoTime.currTime >= deadline) {\n\t\t\t\t\t\tlogCurlWebsocketOutput(\"Timeout waiting for HTTP upgrade response\");\n\t\t\t\t\t\treturn -6;\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\thdrs ~= cast(const(char)[]) tmp[0 .. cast(size_t)got];\n\t\t\t\ttotal += cast(size_t)got;\n\t\t\t\tauto pos = hdrs.indexOf(\"\\r\\n\\r\\n\");\n\t\t\t\tif (pos >= 0) {\n\t\t\t\t\tauto remain = hdrs[(cast(size_t)pos + 4) .. hdrs.length];\n\t\t\t\t\tif (remain.length > 0) {\n\t\t\t\t\t\tauto ru = cast(const(ubyte)[]) remain;\n\t\t\t\t\t\tsize_t old = recvBuf.length;\n\t\t\t\t\t\trecvBuf.length = old + ru.length;\n\t\t\t\t\t\tmemcpy(recvBuf.ptr + old, ru.ptr, ru.length);\n\t\t\t\t\t}\n\t\t\t\t\thdrs = hdrs[0 .. cast(size_t)pos];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (total > maxHdr) {\n\t\t\t\t\tlogCurlWebsocketOutput(\"HTTP upgrade headers too large\");\n\t\t\t\t\treturn -7;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t{\n\t\t\tauto firstLineEnd = hdrs.indexOf(\"\\r\\n\");\n\t\t\tstring statusLine = firstLineEnd > 0 ? hdrs[0 .. cast(size_t)firstLineEnd] : hdrs;\n\t\t\tif (statusLine.indexOf(\"101\") < 0) {\n\t\t\t\tlogCurlWebsocketOutput(\"HTTP upgrade failed; status line: \" ~ statusLine);\n\t\t\t\treturn -8;\n\t\t\t}\n\t\t\tauto low = hdrs.toLower();\n\t\t\tif (low.indexOf(\"upgrade: websocket\") < 0 || low.indexOf(\"connection: upgrade\") < 0) {\n\t\t\t\tlogCurlWebsocketOutput(\"HTTP upgrade missing expected headers\");\n\t\t\t\treturn -9;\n\t\t\t}\n\t\t}\n\n\t\t// Log that protocol switch confirmed, upgraded to RFC6455\n\t\tlogCurlWebsocketOutput(\"Received HTTP 101 Switching Protocols confirmed; Upgraded to RFC6455\");\n\t\twebsocketConnected = true;\n\t\treturn 0;\n\t}\n\n\tint close(ushort code = 1000, string reason = \"\") {\n\t\tlogCurlWebsocketOutput(\"Running curlWebsocket close()\");\n\t\tif (!websocketConnected) {\n\t\t\tlogCurlWebsocketOutput(\"Websocket already closed - websocketConnected = false\");\n\t\t\treturn 0;\n\t\t} else {\n\t\t\tlogCurlWebsocketOutput(\"Running curlWebsocket close() - websocketConnected = true\");\n\t\t}\n\n\t\t// Build close payload: 2 bytes status code (network order) + optional reason\n\t\tubyte[] pay;\n\t\tpay.length = 2 + reason.length;\n\t\tpay[0] = cast(ubyte)((code >> 8) & 0xFF);\n\t\tpay[1] = cast(ubyte)(code & 0xFF);\n\t\tforeach (i; 0 .. reason.length) pay[2 + i] = cast(ubyte)reason[i];\n\n\t\tauto frame = encodeFrame(0x8, pay); // opcode 0x8 = Close\n\t\tauto rc = sendAll(frame);\n\t\t// Even if sending fails, cleanup below so we don’t leak.\n\t\tlogCurlWebsocketOutput(\"Sending RFC6455 Close (code=\" ~ to!string(code) ~ \")\");\n\t\t// Flag we are no longer connected with the websocket\n\t\twebsocketConnected = false;\n\t\treturn rc;\n\t}\n\t\n\t// Cleanup curl handler\n\tvoid cleanupCurlHandle() {\n\t\t// No logging output for this function\n\t\tif (curl !is null) {\n\t\t\tcurl_easy_cleanup(curl);\n\t\t\tcurl = null;\n\t\t}\n\t\twebsocketConnected = false;\n\t}\n\n\tint sendText(string payload) {\n\t\tif (!websocketConnected) return -1;\n\t\tauto frame = encodeFrame(0x1, cast(const(ubyte)[])payload);\n\t\treturn sendAll(frame);\n\t}\n\n\tstring recvText() {\n\t\tif (!websocketConnected) return \"\";\n\n\t\tfor (;;) {\n\t\t\tauto f = tryParseFrame();\n\t\t\tif (f.opcode == 0xFF) {\n\t\t\t\tubyte[4096] tmp;\n\t\t\t\tint got = recvSome(tmp[]);\n\t\t\t\tif (got <= 0) return \"\";\n\t\t\t\tsize_t old = recvBuf.length;\n\t\t\t\trecvBuf.length = old + cast(size_t)got;\n\t\t\t\tmemcpy(recvBuf.ptr + old, tmp.ptr, cast(size_t)got);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (f.opcode == 0x1) {\n\t\t\t\treturn cast(string) f.payload;\n\t\t\t} else if (f.opcode == 0x9) {\n\t\t\t\tauto pong = encodeFrame(0xA, f.payload);\n\t\t\t\tauto _ = sendAll(pong);\n\t\t\t\tcontinue;\n\t\t\t} else if (f.opcode == 0xA) {\n\t\t\t\tcontinue;\n\t\t\t} else if (f.opcode == 0x8) {\n\t\t\t\twebsocketConnected = false;\n\t\t\t\treturn \"\";\n\t\t\t} else {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t}\n\nprivate:\n\tstruct ParsedUrl {\n\t\tbool   ok;\n\t\tstring scheme;\n\t\tstring host;\n\t\tint    port;\n\t\tstring hostPort;\n\t\tstring pathQuery;\n\t}\n\n\tstatic int parsePortDec(string s) {\n\t\tif (s.length == 0) return 0;\n\t\tint v = 0;\n\t\tforeach (ch; s) {\n\t\t\tif (ch < '0' || ch > '9') return 0;\n\t\t\tv = v * 10 + (cast(int)ch - cast(int)'0');\n\t\t\tif (v > 65535) return 0;\n\t\t}\n\t\treturn v;\n\t}\n\n\tParsedUrl parseWsUrl(string u) {\n\t\tParsedUrl p;\n\t\tp.ok = false;\n\t\tauto sidx = u.indexOf(\"://\");\n\t\tif (sidx <= 0) return p;\n\t\tstring sc   = u[0 .. cast(size_t)sidx];\n\t\tstring rest = u[(cast(size_t)sidx + 3) .. u.length];\n\t\tauto scl = sc.toLower();\n\t\tif (scl == \"ws\")  p.scheme = \"ws\";\n\t\telse if (scl == \"wss\") p.scheme = \"wss\";\n\t\telse return p;\n\n\t\tauto slash = rest.indexOf(\"/\");\n\t\tstring hostport;\n\t\tif (slash < 0) {\n\t\t\thostport = rest;\n\t\t\tp.pathQuery = \"/\";\n\t\t} else {\n\t\t\thostport = rest[0 .. cast(size_t)slash];\n\t\t\tp.pathQuery = rest[cast(size_t)slash .. rest.length];\n\t\t}\n\n\t\tauto col = hostport.indexOf(\":\");\n\t\tif (col >= 0) {\n\t\t\tp.host = hostport[0 .. cast(size_t)col];\n\t\t\tstring ps = hostport[(cast(size_t)col + 1) .. hostport.length];\n\n\t\t\tint prt = parsePortDec(ps);\n\t\t\tif (prt == 0) return p;\n\n\t\t\tp.port = prt;\n\t\t\tp.hostPort = p.host ~ \":\" ~ to!string(p.port);\n\t\t} else {\n\t\t\tp.host = hostport;\n\t\t\tp.port = (p.scheme == \"wss\") ? 443 : 80;\n\t\t\tp.hostPort = p.host;\n\t\t}\n\n\t\tif (p.pathQuery.length == 0 || p.pathQuery[0] != '/') p.pathQuery = \"/\" ~ p.pathQuery;\n\n\t\tp.ok = true;\n\t\treturn p;\n\t}\n\n\tstring buildUpgradeRequest() {\n\t\t// Sec-WebSocket-Key: random 16 bytes, base64\n\t\tubyte[16] keyBytes;\n\t\tforeach (i; 0 .. 16) keyBytes[i] = cast(ubyte) uniform(0, 256, rng);\n\t\tauto keyB64 = Base64.encode(keyBytes[]);\n\n\t\t// Origin header (some proxies expect it)\n\t\tstring origin = (scheme == \"wss\" ? \"https://\" : \"http://\") ~ host;\n\n\t\tstring req  = \"GET \" ~ pathQuery ~ \" HTTP/1.1\\r\\n\";\n\t\treq ~= \"Host: \" ~ hostPort ~ \"\\r\\n\";\n\t\treq ~= \"User-Agent: \" ~ userAgent ~ \"\\r\\n\";\n\t\treq ~= \"Upgrade: websocket\\r\\n\";\n\t\treq ~= \"Connection: Upgrade\\r\\n\";\n\t\treq ~= \"Sec-WebSocket-Version: 13\\r\\n\";\n\t\treq ~= \"Sec-WebSocket-Key: \" ~ keyB64 ~ \"\\r\\n\";\n\t\treq ~= \"Origin: \" ~ origin ~ \"\\r\\n\";\n\t\treq ~= \"\\r\\n\";\n\t\treturn req;\n\t}\n\n\tint sendAll(const(char)[] data) {\n\t\tsize_t sent = 0;\n\t\twhile (sent < data.length) {\n\t\t\tsize_t now = 0;\n\t\t\tauto rc = curl_easy_send(curl, cast(void*)(data.ptr + sent), data.length - sent, &now);\n\t\t\tif (rc != 0 && now == 0) return -1;\n\t\t\tsent += now;\n\t\t}\n\t\treturn 0;\n\t}\n\n\tint sendAll(const(ubyte)[] data) {\n\t\tsize_t sent = 0;\n\t\twhile (sent < data.length) {\n\t\t\tsize_t now = 0;\n\t\t\tauto rc = curl_easy_send(curl, cast(void*)(data.ptr + sent), data.length - sent, &now);\n\t\t\tif (rc != 0 && now == 0) return -1;\n\t\t\tsent += now;\n\t\t}\n\t\treturn 0;\n\t}\n\n\tint recvSome(ubyte[] buf) {\n\t\tsize_t got = 0;\n\t\tauto rc = curl_easy_recv(curl, cast(void*)buf.ptr, buf.length, &got);\n\t\tif (rc != 0) return 0; // treat EAGAIN etc. as \"no bytes now\"\n\t\treturn cast(int)got;\n\t}\n\n\tubyte[] encodeFrame(ubyte opcode, const(ubyte)[] payload) {\n\t\tAppender!(ubyte[]) outp = appender!(ubyte[])();\n\t\toutp.reserve(2 + 4 + payload.length + 8);\n\n\t\tubyte b0 = cast(ubyte)(0x80 | (opcode & 0x0F)); // FIN=1\n\t\toutp.put(b0);\n\n\t\tubyte maskBit = 0x80;\n\t\tulong len = cast(ulong)payload.length;\n\n\t\tif (len <= 125) {\n\t\t\toutp.put(cast(ubyte)(maskBit | cast(ubyte)len));\n\t\t} else if (len <= 0xFFFF) {\n\t\t\toutp.put(cast(ubyte)(maskBit | 126));\n\t\t\toutp.put(cast(ubyte)((len >> 8) & 0xFF));\n\t\t\toutp.put(cast(ubyte)(len & 0xFF));\n\t\t} else {\n\t\t\toutp.put(cast(ubyte)(maskBit | 127));\n\t\t\tforeach (shift; AliasSeq!(56, 48, 40, 32, 24, 16, 8, 0)) {\n\t\t\t\toutp.put(cast(ubyte)((len >> shift) & 0xFF));\n\t\t\t}\n\t\t}\n\n\t\tubyte[4] key;\n\t\tforeach (i; 0 .. 4) key[i] = cast(ubyte) uniform(0, 256, rng);\n\t\toutp.put(key[]);\n\n\t\tauto masked = new ubyte[payload.length];\n\t\tforeach (i; 0 .. payload.length) masked[i] = payload[i] ^ key[i % 4];\n\t\toutp.put(masked[]);\n\n\t\treturn outp.data;\n\t}\n\n\tWsFrame tryParseFrame() {\n\t\tWsFrame f;\n\t\tf.opcode = 0xFF;\n\n\t\tif (recvBuf.length < 2) return f;\n\n\t\tsize_t i = 0;\n\t\tubyte b0 = recvBuf[i]; i += 1;\n\t\tubyte b1 = recvBuf[i]; i += 1;\n\n\t\tbool fin = (b0 & 0x80) != 0;\n\t\tubyte opcode = cast(ubyte)(b0 & 0x0F);\n\t\tbool masked = (b1 & 0x80) != 0;\n\t\tulong len = cast(ulong)(b1 & 0x7F);\n\n\t\tif (len == 126) {\n\t\t\tif (recvBuf.length < i + 2) return f;\n\t\t\tlen = (cast(ulong)recvBuf[i] << 8) | cast(ulong)recvBuf[i + 1];\n\t\t\ti += 2;\n\t\t} else if (len == 127) {\n\t\t\tif (recvBuf.length < i + 8) return f;\n\t\t\tlen = 0;\n\t\t\tforeach (shift; AliasSeq!(56, 48, 40, 32, 24, 16, 8, 0)) {\n\t\t\t\tlen |= (cast(ulong)recvBuf[i] << shift);\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t}\n\n\t\tubyte[4] key;\n\t\tif (masked) {\n\t\t\tif (recvBuf.length < i + 4) return f;\n\t\t\tforeach (k; 0 .. 4) key[k] = recvBuf[i + k];\n\t\t\ti += 4;\n\t\t}\n\n\t\tif (recvBuf.length < i + cast(size_t)len) return f;\n\n\t\tauto start = i;\n\t\tauto end   = i + cast(size_t)len;\n\t\tauto raw   = recvBuf[start .. end];\n\n\t\tubyte[] data;\n\t\tif (masked) {\n\t\t\tdata = new ubyte[raw.length];\n\t\t\tforeach (idx; 0 .. raw.length) data[idx] = raw[idx] ^ key[idx % 4];\n\t\t} else {\n\t\t\tdata = raw.dup;\n\t\t}\n\n\t\tauto consumed  = end;\n\t\tauto remainLen = recvBuf.length - consumed;\n\t\tif (remainLen > 0) {\n\t\t\tmemmove(recvBuf.ptr, recvBuf.ptr + consumed, remainLen);\n\t\t}\n\t\trecvBuf.length = remainLen;\n\n\t\tf.fin        = fin ? 1 : 0;\n\t\tf.opcode     = opcode;\n\t\tf.masked     = masked;\n\t\tf.payloadLen = len;\n\t\tf.maskKey    = key;\n\t\tf.payload    = data;\n\t\treturn f;\n\t}\n}\n"
  },
  {
    "path": "src/intune.d",
    "content": "// What is this module called?\nmodule intune;\n\n// What does this module require to function?\nimport core.stdc.string : strcmp;\nimport core.stdc.stdlib : malloc, free;\nimport core.thread : Thread;\nimport core.time : dur;\nimport std.string : fromStringz, toStringz;\nimport std.conv : to;\nimport std.json : JSONValue, parseJSON, JSONType;\nimport std.uuid : randomUUID;\nimport std.range : empty;\nimport std.format : format;\n\n// What 'onedrive' modules do we import?\nimport log;\n\nextern(C):\nalias dbus_bool_t = int;\n\nstruct DBusError {\n    char* name;\n    char* message;\n    uint[8] dummy;\n    void* padding;\n}\n\nstruct DBusConnection;\nstruct DBusMessage;\nstruct DBusMessageIter;\n\nenum DBusBusType {\n    DBUS_BUS_SESSION = 0,\n}\n\nvoid dbus_error_init(DBusError* error);\nvoid dbus_error_free(DBusError* error);\nint dbus_error_is_set(DBusError* error);\nDBusConnection* dbus_bus_get(DBusBusType type, DBusError* error);\ndbus_bool_t dbus_bus_name_has_owner(DBusConnection* conn, const char* name, DBusError* error);\nDBusMessage* dbus_message_new_method_call(const char* dest, const char* path, const char* iface, const char* method);\ndbus_bool_t dbus_connection_send(DBusConnection* conn, DBusMessage* msg, void* client_serial);\nvoid dbus_connection_flush(DBusConnection* conn);\nDBusMessage* dbus_connection_send_with_reply_and_block(DBusConnection* conn, DBusMessage* msg, int timeout_ms, DBusError* error);\nvoid dbus_message_unref(DBusMessage* msg);\ndbus_bool_t dbus_message_iter_init_append(DBusMessage* msg, DBusMessageIter* iter);\ndbus_bool_t dbus_message_iter_append_basic(DBusMessageIter* iter, int type, const void* value);\ndbus_bool_t dbus_message_iter_init(DBusMessage* msg, DBusMessageIter* iter);\ndbus_bool_t dbus_message_iter_get_arg_type(DBusMessageIter* iter);\nvoid dbus_message_iter_get_basic(DBusMessageIter* iter, void* value);\n\nenum DBUS_TYPE_STRING = 115;\nenum DBUS_MESSAGE_ITER_SIZE = 128;\n\nbool check_intune_broker_available() {\n\tversion (linux) {\n\t\tDBusError err;\n\t\tdbus_error_init(&err);\n\t\tDBusConnection* conn = dbus_bus_get(DBusBusType.DBUS_BUS_SESSION, &err);\n\t\tif (dbus_error_is_set(&err)) {\n\t\t\tdbus_error_free(&err);\n\t\t\treturn false;\n\t\t}\n\t\tif (conn is null) return false;\n\t\tdbus_bool_t hasOwner = dbus_bus_name_has_owner(conn, \"com.microsoft.identity.broker1\", &err);\n\t\tif (dbus_error_is_set(&err)) {\n\t\t\tdbus_error_free(&err);\n\t\t\treturn false;\n\t\t}\n\t\treturn hasOwner != 0;\n\t} else {\n\t\treturn false;\n\t}\n}\n\nbool wait_for_broker(int timeoutSeconds = 10) {\n    int waited = 0;\n    while (waited < timeoutSeconds) {\n        if (check_intune_broker_available()) return true;\n        Thread.sleep(dur!\"seconds\"(1));\n        waited++;\n    }\n    return false;\n}\n\nstring build_auth_request(string accountJson = \"\", string clientId = \"\") {\n    string header = format(`{\n  \"authParameters\": {\n    \"clientId\": \"%s\",\n    \"redirectUri\": \"https://login.microsoftonline.com/common/oauth2/nativeclient\",\n    \"authority\": \"https://login.microsoftonline.com/common\",\n    \"requestedScopes\": [\n      \"Files.ReadWrite\",\n      \"Files.ReadWrite.All\",\n      \"Sites.ReadWrite.All\",\n      \"offline_access\"\n    ]`, clientId);\n\n    string footer = `\n  }\n}`;\n\n    if (!accountJson.empty)\n        return header ~ `,\"account\": ` ~ accountJson ~ footer;\n    else\n        return header ~ footer;\n}\n\nstruct AuthResult {\n    JSONValue brokerTokenResponse;\n}\n\n// Initiate interactive authentication via D-Bus using the Microsoft Identity Broker\nAuthResult acquire_token_interactive(string clientId) {\n    AuthResult result;\n\t\n\tversion (linux) {\n\t\tif (!wait_for_broker(10)) {\n\t\t\taddLogEntry(\"Timed out waiting for Identity Broker to appear on D-Bus\");\n\t\t\treturn result;\n\t\t}\n\n\t\t// Step 1: Call acquireTokenInteractively and capture account from result\n\t\tDBusError err;\n\t\tdbus_error_init(&err);\n\t\tDBusConnection* conn = dbus_bus_get(DBusBusType.DBUS_BUS_SESSION, &err);\n\t\tif (dbus_error_is_set(&err) || conn is null) return result;\n\n\t\tDBusMessage* msg = dbus_message_new_method_call(\n\t\t\t\"com.microsoft.identity.broker1\",\n\t\t\t\"/com/microsoft/identity/broker1\",\n\t\t\t\"com.microsoft.identity.Broker1\",\n\t\t\t\"acquireTokenInteractively\"\n\t\t);\n\t\tif (msg is null) return result;\n\n\t\tstring correlationId = randomUUID().toString();\n\t\tstring accountJson = \"\";\n\t\tstring requestJson = build_auth_request(accountJson, clientId);\n\n\t\tDBusMessageIter* args = cast(DBusMessageIter*) malloc(DBUS_MESSAGE_ITER_SIZE);\n\t\tif (!dbus_message_iter_init_append(msg, args)) {\n\t\t\tdbus_message_unref(msg); free(args); return result;\n\t\t}\n\n\t\tconst(char)* protocol = toStringz(\"0.0\");\n\t\tconst(char)* corrId = toStringz(correlationId);\n\t\tconst(char)* reqJson = toStringz(requestJson);\n\n\t\tdbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &protocol);\n\t\tdbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &corrId);\n\t\tdbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &reqJson);\n\t\tfree(args);\n\n\t\tDBusMessage* reply = dbus_connection_send_with_reply_and_block(conn, msg, 60000, &err);\n\t\tdbus_message_unref(msg);\n\n\t\tif (dbus_error_is_set(&err) || reply is null) {\n\t\t\taddLogEntry(\"Interactive call failed\");\n\t\t\treturn result;\n\t\t}\n\n\t\tDBusMessageIter* iter = cast(DBusMessageIter*) malloc(DBUS_MESSAGE_ITER_SIZE);\n\t\tif (!dbus_message_iter_init(reply, iter)) {\n\t\t\tdbus_message_unref(reply); free(iter); return result;\n\t\t}\n\n\t\tchar* responseStr;\n\t\tdbus_message_iter_get_basic(iter, &responseStr);\n\t\tdbus_message_unref(reply); free(iter);\n\n\t\tstring jsonResponse = fromStringz(responseStr).idup;\n\t\tif (debugLogging) {addLogEntry(\"Interactive raw response: \" ~ to!string(jsonResponse), [\"debug\"]);}\n\t\t\n\t\tJSONValue parsed = parseJSON(jsonResponse);\n\t\tif (parsed.type != JSONType.object) return result;\n\t\t\n\t\tauto obj = parsed.object;\n\t\tif (\"brokerTokenResponse\" in obj) {\n\t\t\tresult.brokerTokenResponse = obj[\"brokerTokenResponse\"];\n\t\t}\n\t}\n\n    return result;\n}\n\n// Perform silent authentication via D-Bus using the Microsoft Identity Broker\nAuthResult acquire_token_silently(string accountJson, string clientId) {\n    AuthResult result;\n\t\n\tversion (linux) {\n\t\tDBusError err;\n\t\tdbus_error_init(&err);\n\t\tDBusConnection* conn = dbus_bus_get(DBusBusType.DBUS_BUS_SESSION, &err);\n\t\tif (dbus_error_is_set(&err) || conn is null) return result;\n\n\t\tDBusMessage* msg = dbus_message_new_method_call(\n\t\t\t\"com.microsoft.identity.broker1\",\n\t\t\t\"/com/microsoft/identity/broker1\",\n\t\t\t\"com.microsoft.identity.Broker1\",\n\t\t\t\"acquireTokenSilently\"\n\t\t);\n\t\tif (msg is null) return result;\n\t\t\n\t\tstring correlationId = randomUUID().toString();\n\t\tstring requestJson = build_auth_request(accountJson, clientId);\n\n\t\tDBusMessageIter* args = cast(DBusMessageIter*) malloc(DBUS_MESSAGE_ITER_SIZE);\n\t\tif (!dbus_message_iter_init_append(msg, args)) {\n\t\t\tdbus_message_unref(msg);\n\t\t\tfree(args);\n\t\t\treturn result;\n\t\t}\n\t\t\n\t\tconst(char)* protocol = toStringz(\"0.0\");\n\t\tconst(char)* corrId = toStringz(correlationId);\n\t\tconst(char)* reqJson = toStringz(requestJson);\n\n\t\tdbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &protocol);\n\t\tdbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &corrId);\n\t\tdbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &reqJson);\n\t\tfree(args);\n\t\t\n\t\tDBusMessage* reply = dbus_connection_send_with_reply_and_block(conn, msg, 10000, &err);\n\t\tdbus_message_unref(msg);\n\t\tif (dbus_error_is_set(&err) || reply is null) return result;\n\n\t\tDBusMessageIter* iter = cast(DBusMessageIter*) malloc(DBUS_MESSAGE_ITER_SIZE);\n\t\tif (!dbus_message_iter_init(reply, iter)) {\n\t\t\tdbus_message_unref(reply);\n\t\t\tfree(iter);\n\t\t\treturn result;\n\t\t}\n\t\t\n\t\tchar* responseStr;\n\t\tdbus_message_iter_get_basic(iter, &responseStr);\n\t\tdbus_message_unref(reply);\n\t\tfree(iter);\n\t\t\n\t\tstring jsonResponse = fromStringz(responseStr).idup;\n\t\tif (debugLogging) {addLogEntry(\"Silent raw response: \" ~ to!string(jsonResponse), [\"debug\"]);}\n\t\t\n\t\tJSONValue parsed = parseJSON(jsonResponse);\n\t\tif (parsed.type != JSONType.object) return result;\n\t\t\n\t\tauto obj = parsed.object;\n\t\tif (!(\"brokerTokenResponse\" in obj)) return result;\n\t\t\n\t\tresult.brokerTokenResponse = obj[\"brokerTokenResponse\"];\n\t}\n\t\n    return result;\n}\n"
  },
  {
    "path": "src/itemdb.d",
    "content": "// What is this module called?\nmodule itemdb;\n\n// What does this module require to function?\nimport std.datetime;\nimport std.exception;\nimport std.path;\nimport std.string;\nimport std.stdio;\nimport std.algorithm.searching;\nimport core.stdc.stdlib;\nimport std.json;\nimport std.conv;\nimport core.sync.mutex;\n\n// What other modules that we have created do we need to import?\nimport sqlite;\nimport util;\nimport log;\n\nenum ItemType {\n\tnone,\n\tfile,\n\tdir,\n\tremote,\n\troot,\n\tunknown\n}\n\nstruct Item {\n\tstring   driveId;\n\tstring   id;\n\tstring   name;\n\tstring   remoteName;\n\tItemType type;\n\tstring   eTag;\n\tstring   cTag;\n\tSysTime  mtime;\n\tstring   parentId;\n\tstring   quickXorHash;\n\tstring   sha256Hash;\n\tstring   remoteDriveId;\n\tstring   remoteParentId;\n\tstring   remoteId;\n\tItemType remoteType;\n\tstring   syncStatus;\n\tstring   size;\n\tstring   relocDriveId;\n\tstring   relocParentId;\n}\n\n// Construct an Item DB struct from a JSON driveItem\nItem makeDatabaseItem(JSONValue driveItem) {\n\t\n\tItem item = {\n\t\tid: driveItem[\"id\"].str,\n\t\tname: \"name\" in driveItem ? driveItem[\"name\"].str : null, // name may be missing for deleted files in OneDrive Business\n\t\teTag: \"eTag\" in driveItem ? driveItem[\"eTag\"].str : null, // eTag is not returned for the root in OneDrive Business\n\t\tcTag: \"cTag\" in driveItem ? driveItem[\"cTag\"].str : null, // cTag is missing in old files (and all folders in OneDrive Business)\n\t\tremoteName: \"actualOnlineName\" in driveItem ? driveItem[\"actualOnlineName\"].str : null, // actualOnlineName is only used with OneDrive Business Shared Folders\n\t};\n\n\t// OneDrive API Change: https://github.com/OneDrive/onedrive-api-docs/issues/834\n\t// OneDrive no longer returns lastModifiedDateTime if the item is deleted by OneDrive\n\tif(isItemDeleted(driveItem)) {\n\t\t// Set mtime to SysTime(0)\n\t\titem.mtime = SysTime(0);\n\t} else {\n\t\t// Item is not in a deleted state\n\t\tstring lastModifiedTimestamp;\n\t\t// Resolve 'Key not found: fileSystemInfo' when then item is a remote item\n\t\t// https://github.com/abraunegg/onedrive/issues/11\n\t\tif (isItemRemote(driveItem)) {\n\t\t\t// remoteItem is a OneDrive object that exists on a 'different' OneDrive drive id, when compared to account default\n\t\t\t// Normally, the 'remoteItem' field will contain 'fileSystemInfo' however, if the user uses the 'Add Shortcut ..' option in OneDrive WebUI\n\t\t\t// to create a 'link', this object, whilst remote, does not have 'fileSystemInfo' in the expected place, thus leading to a application crash\n\t\t\t// See: https://github.com/abraunegg/onedrive/issues/1533\n\t\t\tif (\"fileSystemInfo\" in driveItem[\"remoteItem\"]) {\n\t\t\t\t// 'fileSystemInfo' is in 'remoteItem' which will be the majority of cases\n\t\t\t\tlastModifiedTimestamp = strip(driveItem[\"remoteItem\"][\"fileSystemInfo\"][\"lastModifiedDateTime\"].str);\n\t\t\t\t// is lastModifiedTimestamp valid?\n\t\t\t\tif (isValidUTCDateTime(lastModifiedTimestamp)) {\n\t\t\t\t\t// string is a valid timestamp\n\t\t\t\t\titem.mtime = SysTime.fromISOExtString(lastModifiedTimestamp);\n\t\t\t\t} else {\n\t\t\t\t\t// invalid timestamp from JSON file\n\t\t\t\t\taddLogEntry(\"WARNING: Invalid timestamp provided by the Microsoft OneDrive API: \" ~ lastModifiedTimestamp);\n\t\t\t\t\t// Set mtime to Clock.currTime(UTC()) to ensure we have a valid UTC value\n\t\t\t\t\titem.mtime = Clock.currTime(UTC());\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// is a remote item, but 'fileSystemInfo' is missing from 'remoteItem'\n\t\t\t\tlastModifiedTimestamp = strip(driveItem[\"fileSystemInfo\"][\"lastModifiedDateTime\"].str);\n\t\t\t\t// is lastModifiedTimestamp valid?\n\t\t\t\tif (isValidUTCDateTime(lastModifiedTimestamp)) {\n\t\t\t\t\t// string is a valid timestamp\n\t\t\t\t\titem.mtime = SysTime.fromISOExtString(lastModifiedTimestamp);\n\t\t\t\t} else {\n\t\t\t\t\t// invalid timestamp from JSON file\n\t\t\t\t\taddLogEntry(\"WARNING: Invalid timestamp provided by the Microsoft OneDrive API: \" ~ lastModifiedTimestamp);\n\t\t\t\t\t// Set mtime to Clock.currTime(UTC()) to ensure we have a valid UTC value\n\t\t\t\t\titem.mtime = Clock.currTime(UTC());\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Does fileSystemInfo exist at all ?\n\t\t\tif (\"fileSystemInfo\" in driveItem) {\n\t\t\t\t// fileSystemInfo exists\n\t\t\t\tlastModifiedTimestamp = strip(driveItem[\"fileSystemInfo\"][\"lastModifiedDateTime\"].str);\n\t\t\t\t// is lastModifiedTimestamp valid?\n\t\t\t\tif (isValidUTCDateTime(lastModifiedTimestamp)) {\n\t\t\t\t\t// string is a valid timestamp\n\t\t\t\t\titem.mtime = SysTime.fromISOExtString(lastModifiedTimestamp);\n\t\t\t\t} else {\n\t\t\t\t\t// invalid timestamp from JSON file\n\t\t\t\t\taddLogEntry(\"WARNING: Invalid timestamp provided by the Microsoft OneDrive API: \" ~ lastModifiedTimestamp);\n\t\t\t\t\t// Set mtime to Clock.currTime(UTC()) to ensure we have a valid UTC value\n\t\t\t\t\titem.mtime = Clock.currTime(UTC());\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// no timestamp from JSON file\n\t\t\t\taddLogEntry(\"WARNING: No timestamp provided by the Microsoft OneDrive API - using current system time for item!\");\n\t\t\t\t// Set mtime to Clock.currTime(UTC()) to ensure we have a valid UTC value\n\t\t\t\titem.mtime = Clock.currTime(UTC());\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Set this item object type\n\tbool typeSet = false;\n\tif (isItemFile(driveItem)) {\n\t\t// 'file' object exists in the JSON\n\t\tif (debugLogging) {addLogEntry(\"Flagging database item.type as a file\", [\"debug\"]);}\n\t\ttypeSet = true;\n\t\titem.type = ItemType.file;\n\t}\n\t\n\tif (isItemFolder(driveItem)) {\n\t\t// 'folder' object exists in the JSON\n\t\tif (debugLogging) {addLogEntry(\"Flagging database item.type as a directory\", [\"debug\"]);}\n\t\ttypeSet = true;\n\t\titem.type = ItemType.dir;\n\t}\n\t\n\tif (isItemRemote(driveItem)) {\n\t\t// 'remote' object exists in the JSON\n\t\tif (debugLogging) {addLogEntry(\"Flagging database item.type as a remote\", [\"debug\"]);}\n\t\ttypeSet = true;\n\t\titem.type = ItemType.remote;\n\t}\n\n\t// root and remote items do not have parentReference\n\tif (!isItemRoot(driveItem) && (\"parentReference\" in driveItem) != null) {\n\t\titem.driveId = driveItem[\"parentReference\"][\"driveId\"].str;\n\t\tif (hasParentReferenceId(driveItem)) {\n\t\t\titem.parentId = driveItem[\"parentReference\"][\"id\"].str;\n\t\t}\n\t}\n\t\n\t// extract the file hash and file size\n\tif (isItemFile(driveItem) && (\"hashes\" in driveItem[\"file\"])) {\n\t\t// Get file size\n\t\tif (hasFileSize(driveItem)) {\n\t\t\titem.size = to!string(driveItem[\"size\"].integer);\t\n\t\t\t// Get quickXorHash as default\n\t\t\tif (\"quickXorHash\" in driveItem[\"file\"][\"hashes\"]) {\n\t\t\t\titem.quickXorHash = driveItem[\"file\"][\"hashes\"][\"quickXorHash\"].str;\n\t\t\t} else {\n\t\t\t\tif (debugLogging) {addLogEntry(\"quickXorHash is missing from \" ~ driveItem[\"id\"].str, [\"debug\"]);}\n\t\t\t}\n\t\t\t\n\t\t\t// If quickXorHash is empty ..\n\t\t\tif (item.quickXorHash.empty) {\n\t\t\t\t// Is there a sha256Hash?\n\t\t\t\tif (\"sha256Hash\" in driveItem[\"file\"][\"hashes\"]) {\n\t\t\t\t\titem.sha256Hash = driveItem[\"file\"][\"hashes\"][\"sha256Hash\"].str;\n\t\t\t\t} else {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"sha256Hash is missing from \" ~ driveItem[\"id\"].str, [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// So that we have at least a zero value here as the API provided no 'size' data for this file item\n\t\t\titem.size = \"0\";\n\t\t}\n\t}\n\t\n\t// Is the object a remote drive item - living on another driveId ?\n\tif (isItemRemote(driveItem)) {\n\t\t// Check and assign remoteDriveId\n\t\tif (\"parentReference\" in driveItem[\"remoteItem\"] && \"driveId\" in driveItem[\"remoteItem\"][\"parentReference\"]) {\n\t\t\titem.remoteDriveId = driveItem[\"remoteItem\"][\"parentReference\"][\"driveId\"].str;\n\t\t}\n\t\t\n\t\t// Check and assign remoteParentId\n\t\tif (\"parentReference\" in driveItem[\"remoteItem\"] && \"id\" in driveItem[\"remoteItem\"][\"parentReference\"]) {\n\t\t\titem.remoteParentId = driveItem[\"remoteItem\"][\"parentReference\"][\"id\"].str;\n\t\t}\n\t\t\n\t\t// Check and assign remoteId\n\t\tif (\"id\" in driveItem[\"remoteItem\"]) {\n\t\t\titem.remoteId = driveItem[\"remoteItem\"][\"id\"].str;\n\t\t}\n\t\t\n\t\t// Check and assign remoteType\n\t\tif (\"file\" in driveItem[\"remoteItem\"].object) {\n\t\t\titem.remoteType = ItemType.file;\n\t\t} else {\n\t\t\titem.remoteType = ItemType.dir;\n\t\t}\n\t}\n\t\n\t// We have 4 different operational modes where 'item.syncStatus' is used to flag if an item is synced or not:\n\t// - National Cloud Deployments do not support /delta as a query\n\t// - When using --single-directory\n\t// - When using --download-only --cleanup-local-files\n\t// - Are we scanning a Shared Folder\n\t//\n\t// Thus we need to track in the database that this item is in sync\n\t// As we are making an item, set the syncStatus to Y\n\t// ONLY when either of the three modes above are being used, all the existing DB entries will get set to N\n\t// so when processing /children, it can be identified what the 'deleted' difference is\n\titem.syncStatus = \"Y\";\n\n\t// Return the created item\n\treturn item;\n}\n\nfinal class ItemDatabase {\n\t// increment this for every change in the db schema\n\timmutable int itemDatabaseVersion = 18;\n\n\tDatabase db;\n\tstring insertItemStmt;\n\tstring updateItemStmt;\n\tstring deleteOrphanItemStmt;\n\tstring selectItemByIdStmt;\n\tstring selectItemByRemoteIdStmt;\n\tstring selectItemByRemoteDriveIdStmt;\n\tstring selectItemByParentIdStmt;\n\tstring selectRemoteTypeByNameStmt;\n\tstring selectRemoteTypeByRemoteDriveIdStmt;\n\tstring deleteItemByIdStmt;\n\tbool databaseInitialised = false;\n\tprivate Mutex databaseLock;\n\t\n\tthis(string filename) {\n\t\t// Initialise the mutex\n\t\tdatabaseLock = new Mutex();\n\t\t\n\t\tdb = Database(filename);\n\t\tint dbVersion;\n\t\ttry {\n\t\t\tdbVersion = db.getVersion();\n\t\t} catch (SqliteException exception) {\n\t\t\t// An error was generated - what was the error?\n\t\t\t// - database is locked\n\t\t\tif (exception.msg == \"database is locked\" || exception.errorCode == 5) {\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: The 'onedrive' application is already running - please check system process list for active application instances\" , [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry(\" - Use 'sudo ps aufxw | grep onedrive' to potentially determine active running process\");\n\t\t\t\taddLogEntry();\n\t\t\t} else {\n\t\t\t\t// A different error .. detail the message, detail the actual SQLite Error Code to assist with troubleshooting\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: An internal database error occurred: \" ~ exception.msg ~ \" (SQLite Error Code: \" ~ to!string(exception.errorCode) ~ \")\");\n\t\t\t\taddLogEntry();\n\t\t\t\t\n\t\t\t\t// Give the user some additional information and pointers on this error\n\t\t\t\t// The below list is based on user issue / discussion reports since 2018\n\t\t\t\tswitch (exception.errorCode) {\n\t\t\t\t\tcase 7: // SQLITE_NOMEM\n\t\t\t\t\t\taddLogEntry(\"The operation could not be completed due to insufficient memory. Please close unnecessary applications to free up memory and try again.\", [\"info\", \"notify\"]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 10: // SQLITE_IOERR\n\t\t\t\t\t\taddLogEntry(\"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\"]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 11: // SQLITE_CORRUPT\n\t\t\t\t\t\taddLogEntry(\"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\"]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 14: // SQLITE_CANTOPEN\n\t\t\t\t\t\taddLogEntry(\"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\"]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 26: // SQLITE_NOTADB\n\t\t\t\t\t\taddLogEntry(\"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\"]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\taddLogEntry(\"An unexpected error occurred. Please consult the application documentation or request support to resolve this issue.\", [\"info\", \"notify\"]);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t// Blank line before exit\n\t\t\t\taddLogEntry();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tif (dbVersion == 0) {\n\t\t\tcreateTable();\n\t\t} else if (db.getVersion() != itemDatabaseVersion) {\n\t\t\taddLogEntry(\"The item database is incompatible, re-creating database table structures\");\n\t\t\tdb.dropTableIfExists(\"item\");  // Check and drop table if it exists\n\t\t\tcreateTable();\n\t\t}\n\t\t\n\t\t// What is the threadsafe value\n\t\tauto threadsafeValue = db.getThreadsafeValue();\n\t\tif (debugLogging) {addLogEntry(\"SQLite Threadsafe database value: \" ~ to!string(threadsafeValue), [\"debug\"]);}\n\t\t\n\t\ttry {\n\t\t\t// Set the enforcement of foreign key constraints.\n\t\t\t// https://www.sqlite.org/pragma.html#pragma_foreign_keys\n\t\t\t// PRAGMA foreign_keys = boolean;\n\t\t\tdb.exec(\"PRAGMA foreign_keys = TRUE;\");\n\t\t\t// Set the recursive trigger capability\n\t\t\t// https://www.sqlite.org/pragma.html#pragma_recursive_triggers\n\t\t\t// PRAGMA recursive_triggers = boolean;\n\t\t\tdb.exec(\"PRAGMA recursive_triggers = TRUE;\");\n\t\t\t// Set the journal mode for databases associated with the current connection\n\t\t\t// https://www.sqlite.org/pragma.html#pragma_journal_mode\n\t\t\tdb.exec(\"PRAGMA journal_mode = WAL;\");\n\t\t\t// Only checkpoint if WAL grows past a certain size\n\t\t\tdb.exec(\"PRAGMA wal_autocheckpoint = 1000;\");\n\t\t\t// Automatic indexing is enabled by default as of version 3.7.17\n\t\t\t// https://www.sqlite.org/pragma.html#pragma_automatic_index \n\t\t\t// PRAGMA automatic_index = boolean;\n\t\t\tdb.exec(\"PRAGMA automatic_index = FALSE;\");\n\t\t\t// Tell SQLite to store temporary tables in memory. This will speed up many read operations that rely on temporary tables, indices, and views.\n\t\t\t// https://www.sqlite.org/pragma.html#pragma_temp_store\n\t\t\tdb.exec(\"PRAGMA temp_store = MEMORY;\");\n\t\t\t// Tell SQlite to cleanup database table size\n\t\t\t// https://www.sqlite.org/pragma.html#pragma_auto_vacuum\n\t\t\t// PRAGMA schema.auto_vacuum = 0 | NONE | 1 | FULL | 2 | INCREMENTAL;\n\t\t\tdb.exec(\"PRAGMA auto_vacuum = FULL;\");\n\t\t\t// This pragma sets or queries the database connection locking-mode. The locking-mode is either NORMAL or EXCLUSIVE.\n\t\t\t// https://www.sqlite.org/pragma.html#pragma_locking_mode\n\t\t\t// PRAGMA schema.locking_mode = NORMAL | EXCLUSIVE\n\t\t\tdb.exec(\"PRAGMA locking_mode = EXCLUSIVE;\");\n\t\t\t// The synchronous setting determines how carefully SQLite writes data to disk, balancing between performance and data safety.\n\t\t\t// https://sqlite.org/pragma.html#pragma_synchronous\n\t\t\t// PRAGMA synchronous = 0 | OFF | 1 | NORMAL | 2 | FULL | 3 | EXTRA;\n\t\t\tdb.exec(\"PRAGMA synchronous=FULL;\"); // Leave this as FULL, this is the sqlite default, ensure this is set to FULL\n\t\t} catch (SqliteException exception) {\n\t\t\tdetailSQLErrorMessage(exception);\n\t\t} \n\t\t\n\t\tinsertItemStmt = \"\n\t\t\tINSERT OR REPLACE INTO item (driveId, id, name, remoteName, type, eTag, cTag, mtime, parentId, quickXorHash, sha256Hash, remoteDriveId, remoteParentId, remoteId, remoteType, syncStatus, size, relocDriveId, relocParentId)\n\t\t\tVALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)\n\t\t\";\n\t\tupdateItemStmt = \"\n\t\t\tUPDATE item\n\t\t\tSET 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, relocDriveId = ?18, relocParentId = ?19\n\t\t\tWHERE driveId = ?1 AND id = ?2\n\t\t\";\n\t\tdeleteOrphanItemStmt = \"\n\t\t\tDELETE FROM item\n\t\t\tWHERE driveId = ?1 AND parentId = ?9 AND name = ?3\n\t\t\";\n\t\tselectItemByIdStmt = \"\n\t\t\tSELECT *\n\t\t\tFROM item\n\t\t\tWHERE driveId = ?1 AND id = ?2\n\t\t\";\n\t\tselectItemByRemoteIdStmt = \"\n\t\t\tSELECT *\n\t\t\tFROM item\n\t\t\tWHERE remoteDriveId = ?1 AND remoteId = ?2\n\t\t\";\n\t\tselectItemByRemoteDriveIdStmt = \"\n\t\t\tSELECT *\n\t\t\tFROM item\n\t\t\tWHERE remoteDriveId = ?1\n\t\t\";\n\t\tselectRemoteTypeByNameStmt = \"\n\t\t\tSELECT *\n\t\t\tFROM item\n\t\t\tWHERE type = 'remote'\n\t\t\tAND name = ?1\n\t\t\";\n\t\tselectRemoteTypeByRemoteDriveIdStmt = \"\n\t\t\tSELECT *\n\t\t\tFROM item\n\t\t\tWHERE type = 'remote'\n\t\t\tAND remoteDriveId = ?1 AND remoteId = ?2\n\t\t\";\n\t\tselectItemByParentIdStmt = \"SELECT * FROM item WHERE driveId = ? AND parentId = ?\";\n\t\tdeleteItemByIdStmt = \"DELETE FROM item WHERE driveId = ? AND id = ?\";\n\t\t\n\t\t// flag that the database is accessible and we have control\n\t\tdatabaseInitialised = true;\n\t}\n\t\n\t~this() {\n\t\tcloseDatabaseFile();\n\t}\n\t\n\tbool isDatabaseInitialised() {\n\t\treturn databaseInitialised;\n\t}\n\t\n\tvoid closeDatabaseFile() {\n\t\tif (databaseInitialised) {\n\t\t\tdb.close();\n\t\t}\n\t\tdatabaseInitialised = false;\n\t}\n\n\tvoid createTable() {\n\t\tdb.exec(\"CREATE TABLE item (\n\t\t\t\tdriveId          TEXT NOT NULL,\n\t\t\t\tid               TEXT NOT NULL,\n\t\t\t\tname             TEXT NOT NULL,\n\t\t\t\tremoteName       TEXT,\n\t\t\t\ttype             TEXT NOT NULL,\n\t\t\t\teTag             TEXT,\n\t\t\t\tcTag             TEXT,\n\t\t\t\tmtime            TEXT NOT NULL,\n\t\t\t\tparentId         TEXT,\n\t\t\t\tquickXorHash     TEXT,\n\t\t\t\tsha256Hash       TEXT,\n\t\t\t\tremoteDriveId    TEXT,\n\t\t\t\tremoteParentId   TEXT,\n\t\t\t\tremoteId         TEXT,\n\t\t\t\tremoteType       TEXT,\n\t\t\t\tdeltaLink        TEXT,\n\t\t\t\tsyncStatus       TEXT,\n\t\t\t\tsize             TEXT,\n\t\t\t\trelocDriveId     TEXT,\n\t\t\t\trelocParentId    TEXT,\n\t\t\t\tPRIMARY KEY (driveId, id),\n\t\t\t\tFOREIGN KEY (driveId, parentId)\n\t\t\t\tREFERENCES item (driveId, id)\n\t\t\t\tON DELETE CASCADE\n\t\t\t\tON UPDATE RESTRICT\n\t\t\t)\");\n\t\tdb.exec(\"CREATE INDEX name_idx ON item (name)\");\n\t\tdb.exec(\"CREATE INDEX remote_idx ON item (remoteDriveId, remoteId)\");\n\t\tdb.exec(\"CREATE INDEX item_children_idx ON item (driveId, parentId)\");\n\t\tdb.exec(\"CREATE INDEX selectByPath_idx ON item (name, driveId, parentId)\");\n\t\tdb.setVersion(itemDatabaseVersion);\n\t}\n\t\n\tvoid detailSQLErrorMessage(SqliteException exception) {\n\t\taddLogEntry();\n\t\taddLogEntry(\"A database statement execution error occurred: \" ~ exception.msg);\n\t\taddLogEntry();\n\t\t\n\t\tswitch (exception.errorCode) {\n\t\t\tcase 7:   // SQLITE_FULL\n\t\t\tcase 8:   // SQLITE_READONLY\n\t\t\tcase 10:  // SQLITE_SCHEMA\n\t\t\tcase 11:  // SQLITE_CORRUPT\n\t\t\tcase 17:  // SQLITE_IOERR\n\t\t\tcase 21:  // SQLITE_NOMEM\n\t\t\tcase 22:  // SQLITE_MISUSE\n\t\t\tcase 26:  // SQLITE_NOTADB\n\t\t\tcase 27:  // SQLITE_CANTOPEN\n\t\t\t\taddLogEntry(\"Fatal SQLite error encountered. Error code: \" ~ to!string(exception.errorCode), [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry();\n\t\t\t\t// Must exit here\n\t\t\t\tforceExit();\n\t\t\t\t// This line is needed, even though the application technically never gets here ..\n\t\t\t\t// -\tError: switch case fallthrough - use 'goto default;' if intended\n\t\t\t\tgoto default;\n\t\t\tdefault:\n\t\t\t\taddLogEntry(\"Please restart the application with --resync to potentially fix any local database issues.\");\n\t\t\t\t// Handle non-fatal errors or continue execution\n\t\t\t\tbreak;\n\t\t}\n\t}\n\t\n\tvoid insert(const ref Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(insertItemStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\ttry {\n\t\t\t\tbindItem(item, p);\n\t\t\t\tp.exec();\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid update(const ref Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(updateItemStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\ttry {\n\t\t\t\tbindItem(item, p);\n\t\t\t\tp.exec();\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid dump_open_statements() {\n\t\tsynchronized(databaseLock) {\n\t\t\tdb.dump_open_statements();\n\t\t}\n\t}\n\n\tint db_checkpoint() {\n\t\tsynchronized(databaseLock) {\n\t\t\treturn db.db_checkpoint();\n\t\t}\n\t}\n\n\tvoid upsert(const ref Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tStatement selectStmt = db.prepare(\"SELECT COUNT(*) FROM item WHERE driveId = ? AND id = ?\");\n\t\t\tStatement selectParentalStmt = db.prepare(\"SELECT COUNT(*) FROM item WHERE driveId = ? AND parentId = ? AND name = ?\");\n\t\t\tStatement executionStmt = Statement.init;  // Initialise executionStmt to avoid uninitialised variable usage\n\t\t\t\n\t\t\tscope(exit) {\n\t\t\t\tselectStmt.finalise();\n\t\t\t\tselectParentalStmt.finalise();\n\t\t\t\texecutionStmt.finalise();\n\t\t\t}\n\t\t\t\n\t\t\ttry {\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"Attempting upsert for item: driveId='\" ~ item.driveId ~ \"', id='\" ~ item.id ~ \"', parentId='\" ~ item.parentId ~ \"', name='\" ~ item.name ~ \"'\", [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tselectStmt.bind(1, item.driveId);\n\t\t\t\tselectStmt.bind(2, item.id);\n\t\t\t\tauto result = selectStmt.exec();\n\t\t\t\tsize_t count = result.front[0].to!size_t;\n\t\t\t\t\n\t\t\t\t// If the existing 'driveId' and 'id' are in the DB, then this is a record to update\n\t\t\t\tif (count == 0) {\n\t\t\t\t\t// Item with id not found, check for orphaned entry by parentId and name\n\t\t\t\t\t// - If the user has deleted and recreated the folder online with the same name, whilst we may have an existing entry, this will have the old 'id'\n\t\t\t\t\tselectParentalStmt.bind(1, item.driveId);\n\t\t\t\t\tselectParentalStmt.bind(2, item.parentId);\n\t\t\t\t\tselectParentalStmt.bind(3, item.name);\n\t\t\t\t\tauto orphanResult = selectParentalStmt.exec();\n\t\t\t\t\tsize_t orphanCount = orphanResult.front[0].to!size_t;\n\n\t\t\t\t\t// Were orphans found?\n\t\t\t\t\tif (orphanCount == 0) {\n\t\t\t\t\t\t// No match on name+parentId either — new insert\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"Inserting new item: driveId='\" ~ item.driveId ~ \"', id='\" ~ item.id ~ \"', parentId='\" ~ item.parentId ~ \"', name='\" ~ item.name ~ \"'\", [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\texecutionStmt = db.prepare(insertItemStmt);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Orphans found\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"Orphan lookup: count=\" ~ to!string(orphanCount) ~ \" for driveId='\" ~ item.driveId ~ \"', parentId='\" ~ item.parentId ~ \"', name='\" ~ item.name ~ \"'\", [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"Orphaned DB Entry - deleting old entry for name='\" ~ item.name ~ \"' and parentId='\" ~ item.parentId ~ \"'\", [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t\t// Orphan exists, delete it first\n\t\t\t\t\t\tauto deleteOrphan = db.prepare(deleteOrphanItemStmt);\n\t\t\t\t\t\tdeleteOrphan.bind(1, item.driveId);\n\t\t\t\t\t\tdeleteOrphan.bind(2, item.parentId);\n\t\t\t\t\t\tdeleteOrphan.bind(3, item.name);\n\t\t\t\t\t\tdeleteOrphan.exec();\n\t\t\t\t\t\tdeleteOrphan.finalise();\n\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"Deleted orphaned entry — now inserting new item: id='\" ~ item.id ~ \"', parentId='\" ~ item.parentId ~ \"', name='\" ~ item.name ~ \"'\", [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\texecutionStmt = db.prepare(insertItemStmt);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Found by ID — perform update\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"Updating existing DB record: id='\" ~ item.id ~ \"', parentId='\" ~ item.parentId ~ \"', name='\" ~ item.name ~ \"'\", [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\texecutionStmt = db.prepare(updateItemStmt);\n\t\t\t\t}\n\n\t\t\t\tbindItem(item, executionStmt);\n\t\t\t\texecutionStmt.exec();\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle errors appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t}\n\t}\n\n\tItem[] selectChildren(const(char)[] driveId, const(char)[] id) {\n\t\tsynchronized(databaseLock) {\n\t\t\tItem[] items;\n\t\t\tauto p = db.prepare(selectItemByParentIdStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\t\n\t\t\ttry {\n\t\t\t\tp.bind(1, driveId);\n\t\t\t\tp.bind(2, id);\n\t\t\t\tauto res = p.exec();\n\t\t\t\t\n\t\t\t\twhile (!res.empty) {\n\t\t\t\t\titems ~= buildItem(res);\n\t\t\t\t\tres.step();\n\t\t\t\t}\n\t\t\t\treturn items;\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle errors appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t\titems = [];\n\t\t\t\treturn items; // Return an empty array on error\n\t\t\t}\n\t\t}\n\t}\n\n\tbool selectById(const(char)[] driveId, const(char)[] id, out Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(selectItemByIdStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\t\n\t\t\ttry {\n\t\t\t\tp.bind(1, driveId);\n\t\t\t\tp.bind(2, id);\n\t\t\t\tauto r = p.exec();\n\t\t\t\tif (!r.empty) {\n\t\t\t\t\titem = buildItem(r);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tbool selectByRemoteId(const(char)[] remoteDriveId, const(char)[] remoteId, out Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(selectItemByRemoteIdStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\ttry {\n\t\t\t\tp.bind(1, remoteDriveId);\n\t\t\t\tp.bind(2, remoteId);\n\t\t\t\tauto r = p.exec();\n\t\t\t\tif (!r.empty) {\n\t\t\t\t\titem = buildItem(r);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\n\t\t\treturn false;\n\t\t}\n\t}\n\t\n\t// This should return the 'remote' DB entry for a given remote drive id\n\tbool selectByRemoteDriveId(const(char)[] remoteDriveId, out Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(selectItemByRemoteDriveIdStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\ttry {\n\t\t\t\tp.bind(1, remoteDriveId);\n\t\t\t\tauto r = p.exec();\n\t\t\t\tif (!r.empty) {\n\t\t\t\t\titem = buildItem(r);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n\t\n\t// This should return the 'remote' DB entry for the given 'name'\n\tbool selectByRemoteEntryByName(const(char)[] entryName, out Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(selectRemoteTypeByNameStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\ttry {\n\t\t\t\tp.bind(1, entryName);\n\t\t\t\tauto r = p.exec();\n\t\t\t\tif (!r.empty) {\n\t\t\t\t\titem = buildItem(r);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// This should return the 'remote' DB entry for the given 'remoteDriveId' and 'remoteId'\n\tbool selectRemoteTypeByRemoteDriveId(const(char)[] remoteDriveId, const(char)[] remoteId, out Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(selectRemoteTypeByRemoteDriveIdStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\ttry {\n\t\t\t\tp.bind(1, remoteDriveId);\n\t\t\t\tp.bind(2, remoteId);\n\t\t\t\tauto r = p.exec();\n\t\t\t\tif (!r.empty) {\n\t\t\t\t\titem = buildItem(r);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// returns true if an item id is in the database\n\tbool idInLocalDatabase(const(string) driveId, const(string) id) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(selectItemByIdStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\ttry {\n\t\t\t\tp.bind(1, driveId);\n\t\t\t\tp.bind(2, id);\n\t\t\t\tauto r = p.exec();\n\t\t\t\treturn !r.empty;\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\t// returns the item with the given path\n\t// the path is relative to the sync directory ex: \"./Music/file_name.mp3\"\n\tbool selectByPath(const(char)[] path, string rootDriveId, out Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tItem currItem = { driveId: rootDriveId };\n\t\t\t\n\t\t\t// Issue https://github.com/abraunegg/onedrive/issues/578\n\t\t\tpath = \"root/\" ~ (startsWith(path, \"./\") || path == \".\" ? path.chompPrefix(\".\") : path);\n\t\t\t\n\t\t\tauto s = db.prepare(\"SELECT * FROM item WHERE name = ?1 AND driveId IS ?2 AND parentId IS ?3\");\n\t\t\tscope(exit) s.finalise(); // Ensure that the prepared statement is finalised after execution.\n\n\t\t\ttry {\n\t\t\t\tforeach (name; pathSplitter(path)) {\n\t\t\t\t\ts.bind(1, name);\n\t\t\t\t\ts.bind(2, currItem.driveId);\n\t\t\t\t\ts.bind(3, currItem.id);\n\t\t\t\t\tauto r = s.exec();\n\t\t\t\t\tif (r.empty) return false;\n\t\t\t\t\tcurrItem = buildItem(r);\n\t\t\t\t\t\n\t\t\t\t\t// If the item is of type remote, substitute it with the child\n\t\t\t\t\tif (currItem.type == ItemType.remote) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Record is a Remote Object: \" ~ to!string(currItem), [\"debug\"]);}\n\t\t\t\t\t\tItem child;\n\t\t\t\t\t\tif (selectById(currItem.remoteDriveId, currItem.remoteId, child)) {\n\t\t\t\t\t\t\tassert(child.type != ItemType.remote, \"The type of the child cannot be remote\");\n\t\t\t\t\t\t\tcurrItem = child;\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Selecting Record that is NOT Remote Object: \" ~ to!string(currItem), [\"debug\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\titem = currItem;\n\t\t\t\treturn true;\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\t// same as selectByPath() but it does not traverse remote folders, returns the remote element if that is what is required\n\tbool selectByPathIncludingRemoteItems(const(char)[] path, string rootDriveId, out Item item) {\n\t\tsynchronized(databaseLock) {\n\t\t\tItem currItem = { driveId: rootDriveId };\n\n\t\t\t// Issue https://github.com/abraunegg/onedrive/issues/578\n\t\t\tpath = \"root/\" ~ (startsWith(path, \"./\") || path == \".\" ? path.chompPrefix(\".\") : path);\n\n\t\t\tauto s = db.prepare(\"SELECT * FROM item WHERE name IS ?1 AND driveId IS ?2 AND parentId IS ?3\");\n\t\t\tscope(exit) s.finalise(); // Ensure that the prepared statement is finalised after execution.\n\n\t\t\ttry {\n\t\t\t\tforeach (name; pathSplitter(path)) {\n\t\t\t\t\ts.bind(1, name);\n\t\t\t\t\ts.bind(2, currItem.driveId);\n\t\t\t\t\ts.bind(3, currItem.id);\n\t\t\t\t\tauto r = s.exec();\n\t\t\t\t\tif (r.empty) return false;\n\t\t\t\t\tcurrItem = buildItem(r);\n\t\t\t\t}\n\n\t\t\t\tif (currItem.type == ItemType.remote) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Record selected is a Remote Object: \" ~ to!string(currItem), [\"debug\"]);}\n\t\t\t\t}\n\n\t\t\t\titem = currItem;\n\t\t\t\treturn true;\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid deleteById(const(char)[] driveId, const(char)[] id) {\n\t\tsynchronized(databaseLock) {\n\t\t\tauto p = db.prepare(deleteItemByIdStmt);\n\t\t\tscope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\ttry {\n\t\t\t\tp.bind(1, driveId);\n\t\t\t\tp.bind(2, id);\n\t\t\t\tp.exec();\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void bindItem(const ref Item item, ref Statement stmt) {\n\t\twith (stmt) with (item) {\n\t\t\tbind(1, driveId);\n\t\t\tbind(2, id);\n\t\t\tbind(3, name);\n\t\t\tbind(4, remoteName);\n\t\t\t// type handling\n\t\t\tstring typeStr = null;\n\t\t\tfinal switch (type) with (ItemType) {\n\t\t\t\tcase file:    typeStr = \"file\";    break;\n\t\t\t\tcase dir:     typeStr = \"dir\";     break;\n\t\t\t\tcase remote:  typeStr = \"remote\";  break;\n\t\t\t\tcase root:    typeStr = \"root\";    break;\n\t\t\t\tcase unknown: typeStr = \"unknown\"; break;\n\t\t\t\tcase none:    typeStr = null; break;\n\t\t\t}\n\t\t\tbind(5, typeStr);\n\t\t\tbind(6, eTag);\n\t\t\tbind(7, cTag);\n\t\t\tbind(8, mtime.toISOExtString());\n\t\t\tbind(9, parentId);\n\t\t\tbind(10, quickXorHash);\n\t\t\tbind(11, sha256Hash);\n\t\t\tbind(12, remoteDriveId);\n\t\t\tbind(13, remoteParentId);\n\t\t\tbind(14, remoteId);\n\t\t\t// remoteType handling\n\t\t\tstring remoteTypeStr = null;\n\t\t\tfinal switch (remoteType) with (ItemType) {\n\t\t\t\tcase file:    remoteTypeStr = \"file\";    break;\n\t\t\t\tcase dir:     remoteTypeStr = \"dir\";     break;\n\t\t\t\tcase remote:  remoteTypeStr = \"remote\";  break;\n\t\t\t\tcase root:    remoteTypeStr = \"root\";    break;\n\t\t\t\tcase unknown: remoteTypeStr = \"unknown\"; break;\n\t\t\t\tcase none:    remoteTypeStr = null; break;\n\t\t\t}\n\t\t\tbind(15, remoteTypeStr);\n\t\t\tbind(16, syncStatus);\n\t\t\tbind(17, size);\n\t\t\tbind(18, relocDriveId);\n\t\t\tbind(19, relocParentId);\n\t\t}\n\t}\n\n\tprivate Item buildItem(Statement.Result result) {\n\t\tassert(!result.empty, \"The DB result must not be empty\");\n\t\tassert(result.front.length == 20, \"The DB result must have 20 columns\");\n\t\t\n\t\t// Check the DB record timestamp entry. Rather than assert(), use forceExit() and exit in a more graceful manner\n\t\t// - empty values\n\t\t// - 2024-11-23T01:16:14\\x80Z\n\t\t// - ��Ϣc (#3014)\n\t\t// - ����� (#2876)\n\t\t// - non timestamp formatted strings such as 'CurlEngine curlEngin' (#2813)\n\t\tif (!isValidUTCDateTime(result.front[7].dup)) {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"FATAL: The DB record mtime entry is not a valid ISO timestamp entry. Please attempt a --resync to fix the local database.\");\n\t\t\taddLogEntry();\n\t\t\t// Must force exit here, allow logging to be done\n\t\t\tforceExit();\n\t\t}\n\t\t\n\t\tItem item = {\n\t\t\t// column 0: driveId\n\t\t\t// column 1: id\n\t\t\t// column 2: name\n\t\t\t// column 3: remoteName - only used when there is a difference in the local name & remote shared folder name\n\t\t\t// column 4: type\n\t\t\t// column 5: eTag\n\t\t\t// column 6: cTag\n\t\t\t// column 7: mtime\n\t\t\t// column 8: parentId\n\t\t\t// column 9: quickXorHash\n\t\t\t// column 10: sha256Hash\n\t\t\t// column 11: remoteDriveId\n\t\t\t// column 12: remoteParentId\n\t\t\t// column 13: remoteId\n\t\t\t// column 14: remoteType\n\t\t\t// column 15: deltaLink\n\t\t\t// column 16: syncStatus\n\t\t\t// column 17: size\n\t\t\t// column 18: relocDriveId\n\t\t\t// column 19: relocParentId\n\t\t\t\t\n\t\t\tdriveId: result.front[0].dup,\n\t\t\tid: result.front[1].dup,\n\t\t\tname: result.front[2].dup,\n\t\t\tremoteName: result.front[3].dup,\n\t\t\t// Column 4 is type - not set here\n\t\t\teTag: result.front[5].dup,\n\t\t\tcTag: result.front[6].dup,\n\t\t\tmtime: SysTime.fromISOExtString(result.front[7].dup),\n\t\t\tparentId: result.front[8].dup,\n\t\t\tquickXorHash: result.front[9].dup,\n\t\t\tsha256Hash: result.front[10].dup,\n\t\t\tremoteDriveId: result.front[11].dup,\n\t\t\tremoteParentId: result.front[12].dup,\n\t\t\tremoteId: result.front[13].dup,\n\t\t\t// Column 14 is remoteType - not set here\n\t\t\t// Column 15 is deltaLink - not set here\n\t\t\tsyncStatus: result.front[16].dup,\n\t\t\tsize: result.front[17].dup,\n\t\t\trelocDriveId: result.front[18].dup,\n\t\t\trelocParentId: result.front[19].dup\n\t\t};\n\t\t\n\t\t// Configure item.type\n\t\tswitch (result.front[4]) {\n\t\t\tcase \"file\":    item.type = ItemType.file;    break;\n\t\t\tcase \"dir\":     item.type = ItemType.dir;     break;\n\t\t\tcase \"remote\":  item.type = ItemType.remote;  break;\n\t\t\tcase \"root\":    item.type = ItemType.root;    break;\n\t\t\tdefault: assert(0, \"Invalid item type\");\n\t\t}\n\t\t\n\t\t// Configure item.remoteType\n\t\tswitch (result.front[14]) {\n\t\t\t// We only care about 'dir' and 'file' for 'remote' items\n\t\t\tcase \"file\":    item.remoteType = ItemType.file;    break;\n\t\t\tcase \"dir\":     item.remoteType = ItemType.dir;     break;\n\t\t\tdefault: item.remoteType = ItemType.none;    break; // Default to ItemType.none\n\t\t}\n\t\t\n\t\t// Return item\n\t\treturn item;\n\t}\n\n\t// Computes the relative path of the given item ID as stored in the OneDrive item database.\n\t//\n\t// The path is reconstructed by traversing the item's parent hierarchy via parentId,\n\t// optionally resolving relocation fields (relocDriveId and relocParentId) if present.\n\t// The returned path is relative to the configured sync directory, e.g. \"Music/Turbo Killer.mp3\".\n\t//\n\t// Behaviour includes:\n\t// - Handling normal items and directory structures\n\t// - Supporting relocated shared folder roots via relocDriveId and relocParentId\n\t// - Skipping inclusion of any item with ItemType.root to avoid adding \"root\" as a path segment\n\t// - Ensuring folders named \"root\" (with ItemType.dir) are still correctly included\n\t//\n\t// Note: The returned path does not end with a trailing slash, even for directories.\n\tstring computePath(const(char)[] driveIdInput, const(char)[] itemIdInput) {\n\t\tsynchronized(databaseLock) {\n\t\t\tassert(driveIdInput && itemIdInput);\n\n\t\t\tstring path;\n\t\t\tstring driveId = driveIdInput.idup;\n\t\t\tstring id = itemIdInput.idup;\n\t\t\tItem item;\n\n\t\t\t// Remember the highest non-root node we saw in this drive\n\t\t\tstring anchorCandidateDriveId;\n\t\t\tstring anchorCandidateItemId;\n\n\t\t\t// DB Statements\n\t\t\tauto s = db.prepare(\"SELECT * FROM item WHERE driveId = ?1 AND id = ?2\");\n\t\t\tauto s2 = db.prepare(\"SELECT driveId, id FROM item WHERE remoteDriveId = ?1 AND remoteId = ?2\");\n\n\t\t\tscope(exit) {\n\t\t\t\ts.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\t\ts2.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\t}\n\t\t\t\n\t\t\t// Attempt to compute the path based on the elements provided\n\t\t\ttry {\n\t\t\t\twhile (true) {\n\t\t\t\t\ts.bind(1, driveId);\n\t\t\t\t\ts.bind(2, id);\n\t\t\t\t\tauto r = s.exec();\n\n\t\t\t\t\tif (!r.empty) {\n\t\t\t\t\t\titem = buildItem(r);\n\n\t\t\t\t\t\t// Track the highest non-root row we encounter\n\t\t\t\t\t\tif (item.type != ItemType.root) {\n\t\t\t\t\t\t\tanchorCandidateDriveId = driveId;\n\t\t\t\t\t\t\tanchorCandidateItemId  = item.id;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Build path: Skip only if name == \"root\" AND item.type == ItemType.root\n\t\t\t\t\t\tconst bool skipAppend = (item.name == \"root\") && (item.type == ItemType.root);\n\t\t\t\t\t\tif (!skipAppend) {\n\t\t\t\t\t\t\tif (item.type == ItemType.remote) {\n\t\t\t\t\t\t\t\t// replace first segment with remote name\n\t\t\t\t\t\t\t\tauto idx = indexOf(path, '/');\n\t\t\t\t\t\t\t\tpath = (idx >= 0) ? item.name ~ path[idx .. $] : item.name;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tpath = path.length ? item.name ~ \"/\" ~ path : item.name;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Move up one level (within the same drive)\n\t\t\t\t\t\tid = item.parentId;\n\n\t\t\t\t\t\t// Check for relocation and handle the relocation\n\t\t\t\t\t\tif (item.type == ItemType.root && item.relocDriveId !is null && item.relocParentId !is null) {\n\t\t\t\t\t\t\tdriveId = item.relocDriveId;\n\t\t\t\t\t\t\tid = item.relocParentId;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// We fell off the top (id == null). Try to jump to the anchor (mount point).\n\t\t\t\t\t\tif (id == null) {\n\t\t\t\t\t\t\t// Use the top-most NON-ROOT we saw, not the root we just processed\n\t\t\t\t\t\t\tif (anchorCandidateItemId.length) {\n\t\t\t\t\t\t\t\ts2.bind(1, anchorCandidateDriveId);  // remoteDriveId\n\t\t\t\t\t\t\t\ts2.bind(2, anchorCandidateItemId);   // remoteId (top-most folder)\n\t\t\t\t\t\t\t\tauto r2 = s2.exec();\n\t\t\t\t\t\t\t\tif (r2.empty) {\n\t\t\t\t\t\t\t\t\tbreak; // no anchor -> done\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Jump into the drive that contains the remote mount point\n\t\t\t\t\t\t\t\t\tdriveId = r2.front[0].dup;\n\t\t\t\t\t\t\t\t\tid      = r2.front[1].dup;\n\t\t\t\t\t\t\t\t\t// loop continues; next iteration will fetch the 'remote' row\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// no candidate (single item or broken tree)\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// broken database tree\n\t\t\t\t\t\t\taddLogEntry(\"The following generated a broken database tree query:\");\n\t\t\t\t\t\t\taddLogEntry(\"Drive ID: \" ~ to!string(driveId));\n\t\t\t\t\t\t\taddLogEntry(\"Item ID: \" ~ to!string(id));\n\t\t\t\t\t\t\tassert(0);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\n\t\t\tif (path.length == 0) {\n\t\t\t\tpath = \".\";\n\t\t\t}\n\t\t\t\n\t\t\t// Return the computed path\n\t\t\treturn path;\n\t\t}\n\t}\n\n\tItem[] selectRemoteItems() {\n\t\tsynchronized(databaseLock) {\n\t\t\tItem[] items;\n\t\t\tauto stmt = db.prepare(\"SELECT * FROM item WHERE remoteDriveId IS NOT NULL\");\n\t\t\tscope (exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution.\n\n\t\t\ttry {\n\t\t\t\tauto res = stmt.exec();\n\t\t\t\twhile (!res.empty) {\n\t\t\t\t\titems ~= buildItem(res);\n\t\t\t\t\tres.step();\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t\treturn items;\n\t\t}\n\t}\n\n\tstring getDeltaLink(const(char)[] driveId, const(char)[] id) {\n\t\tsynchronized(databaseLock) {\n\t\t\t// Log what we received\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"DeltaLink Query (driveId): \" ~ to!string(driveId), [\"debug\"]);\n\t\t\t\taddLogEntry(\"DeltaLink Query (id):      \" ~ to!string(id), [\"debug\"]);\n\t\t\t}\n\t\t\t// assert if these are null\n\t\t\tassert(driveId && id);\n\t\t\t\n\t\t\tauto stmt = db.prepare(\"SELECT deltaLink FROM item WHERE driveId = ?1 AND id = ?2\");\n\t\t\tscope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution.\n\n\t\t\ttry {\n\t\t\t\tstmt.bind(1, driveId);\n\t\t\t\tstmt.bind(2, id);\n\t\t\t\tauto res = stmt.exec();\n\t\t\t\tif (res.empty) return null;\n\t\t\t\treturn res.front[0].dup;\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t}\n\t\n\tvoid setDeltaLink(const(char)[] driveId, const(char)[] id, const(char)[] deltaLink) {\n\t\tsynchronized(databaseLock) {\n\t\t\tassert(driveId && id);\n\t\t\tassert(deltaLink);\n\t\t\t\n\t\t\tauto stmt = db.prepare(\"UPDATE item SET deltaLink = ?3 WHERE driveId = ?1 AND id = ?2\");\n\t\t\tscope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\t\n\t\t\ttry {\n\t\t\t\tstmt.bind(1, driveId);\n\t\t\t\tstmt.bind(2, id);\n\t\t\t\tstmt.bind(3, deltaLink);\n\t\t\t\tstmt.exec();\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// We have 4 different operational modes where 'item.syncStatus' is used to flag if an item is synced or not:\n\t// - National Cloud Deployments do not support /delta as a query\n\t// - When using --single-directory\n\t// - When using --download-only --cleanup-local-files\n\t// - Are we scanning a Shared Folder\n\t//\n\t// As we query /children to get all children from OneDrive, update anything in the database \n\t// to be flagged as not-in-sync, thus, we can use that flag to determine what was previously\n\t// in-sync, but now deleted on OneDrive\n\tvoid downgradeSyncStatusFlag(const(char)[] driveId, const(char)[] id) {\n\t\tsynchronized(databaseLock) {\n\t\t\tassert(driveId);\n\n\t\t\tauto stmt = db.prepare(\"UPDATE item SET syncStatus = 'N' WHERE driveId = ?1 AND id = ?2\");\n\t\t\tscope(exit) {\n\t\t\t\tstmt.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tstmt.bind(1, driveId);\n\t\t\t\tstmt.bind(2, id);\n\t\t\t\tstmt.exec();\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// We have 4 different operational modes where 'item.syncStatus' is used to flag if an item is synced or not:\n\t// - National Cloud Deployments do not support /delta as a query\n\t// - When using --single-directory\n\t// - When using --download-only --cleanup-local-files\n\t// - Are we scanning a Shared Folder\n\t//\n\t// Select items that have a out-of-sync flag set\n\tItem[] selectOutOfSyncItems(const(char)[] driveId) {\n\t\tsynchronized(databaseLock) {\n\t\t\tassert(driveId);\n\t\t\tItem[] items;\n\t\t\tauto stmt = db.prepare(\"SELECT * FROM item WHERE syncStatus = 'N' AND driveId = ?1\");\n\t\t\tscope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\t\n\t\t\ttry {\n\t\t\t\tstmt.bind(1, driveId);\n\t\t\t\tauto res = stmt.exec();\n\t\t\t\twhile (!res.empty) {\n\t\t\t\t\titems ~= buildItem(res);\n\t\t\t\t\tres.step();\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t\treturn items;\n\t\t}\n\t}\n\n\t// OneDrive Business Folders are stored in the database potentially without a root | parentRoot link\n\t// Select items associated with the provided driveId\n\tItem[] selectByDriveId(const(char)[] driveId) {\n\t\tsynchronized(databaseLock) {\n\t\t\tassert(driveId);\n\t\t\tItem[] items;\n\t\t\tauto stmt = db.prepare(\"SELECT * FROM item WHERE driveId = ?1 AND parentId IS NULL\");\n\t\t\tscope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution.\n\n\t\t\ttry {\n\t\t\t\tstmt.bind(1, driveId);\n\t\t\t\tauto res = stmt.exec();\n\t\t\t\twhile (!res.empty) {\n\t\t\t\t\titems ~= buildItem(res);\n\t\t\t\t\tres.step();\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t\treturn items;\n\t\t}\n\t}\n\n\t// Perform a vacuum on the database, commit WAL / SHM to file\n\tvoid performVacuum() {\n\t\tsynchronized(databaseLock) {\n\t\t\t// Log what we are attempting to do\n\t\t\taddLogEntry(\"Attempting to perform a database vacuum to optimise database\");\n\t\t\t\n\t\t\ttry {\n\t\t\t\t// Check the current DB Status - we have to be in a clean state here\n\t\t\t\tdb.checkStatus();\n\t\t\t\t\n\t\t\t\t// Are there any open statements that need to be closed?\n\t\t\t\tif (db.count_open_statements() > 0) {\n\t\t\t\t\t// Dump open statements\n\t\t\t\t\tdb.dump_open_statements(); // dump open statements so we know what the are\n\t\t\t\t\t\n\t\t\t\t\t// SIGINT (CTRL-C), SIGTERM (kill) handling\n\t\t\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t\t\t// The SQLITE_INTERRUPT result code indicates that an operation was interrupted - which if we have open statements, most likely a SIGINT scenario\n\t\t\t\t\t\tthrow new SqliteException(9, \"Open SQL Statements due to interrupted operations\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Try and close open statements\n\t\t\t\t\t\tdb.close_open_statements();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Ensure there are no pending operations by performing a PASSIVE checkpoint\n\t\t\t\tdb.exec(\"PRAGMA wal_checkpoint(PASSIVE);\");\n\t\t\t\t\n\t\t\t\t// Prepare and execute VACUUM statement\n\t\t\t\tStatement stmt = db.prepare(\"VACUUM;\");\n\t\t\t\tscope(exit) stmt.finalise();  // Ensure the statement is finalised when we exit\n\t\t\t\tstmt.exec();\n\t\t\t\taddLogEntry(\"Database vacuum is complete\");\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: Unable to perform a database vacuum: \" ~ exception.msg);\n\t\t\t\taddLogEntry();\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Perform a checkpoint (either TRUNCATE or PASSIVE) by writing the data into to the database from the WAL file\n\tvoid performCheckpoint(string checkpointType) {\n\t\tsynchronized(databaseLock) {\n\t\t\t// Log what we are attempting to do\n\t\t\tif (debugLogging) {addLogEntry(\"Attempting to perform a database checkpoint to merge temporary data\", [\"debug\"]);}\n\t\t\t\n\t\t\ttry {\n\t\t\t\t// Check the current DB Status - we have to be in a clean state here\n\t\t\t\tdb.checkStatus();\n\t\t\t\t\n\t\t\t\t// Are there any open statements that need to be closed?\n\t\t\t\tif (db.count_open_statements() > 0) {\n\t\t\t\t\t// Dump open statements\n\t\t\t\t\tdb.dump_open_statements(); // dump open statements so we know what the are\n\t\t\t\t\t\n\t\t\t\t\t// SIGINT (CTRL-C), SIGTERM (kill) handling\n\t\t\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t\t\t// The SQLITE_INTERRUPT result code indicates that an operation was interrupted - which if we have open statements, most likely a SIGINT scenario\n\t\t\t\t\t\tthrow new SqliteException(9, \"Open SQL Statements due to interrupted operations\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Try and close open statements\n\t\t\t\t\t\tdb.close_open_statements();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Ensure there are no pending operations by performing a checkpoint\n\t\t\t\tstring databaseCommand = format(\"PRAGMA wal_checkpoint(%s);\" , checkpointType);\n\t\t\t\tdb.exec(databaseCommand);\n\t\t\t\tif (debugLogging) {addLogEntry(\"Database checkpoint is complete\", [\"debug\"]);}\n\t\t\t\t\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: Unable to perform a database checkpoint: \" ~ exception.msg);\n\t\t\t\taddLogEntry();\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Select distinct driveId items from database\n\tstring[] selectDistinctDriveIds() {\n\t\tsynchronized(databaseLock) {\n\t\t\tstring[] driveIdArray;\n\t\t\tauto stmt = db.prepare(\"SELECT DISTINCT driveId FROM item;\");\n\t\t\tscope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution.\n\t\t\t\n\t\t\ttry {\n\t\t\t\tauto res = stmt.exec();\n\t\t\t\tif (res.empty) return driveIdArray;\n\t\t\t\twhile (!res.empty) {\n\t\t\t\t\tdriveIdArray ~= res.front[0].dup;\n\t\t\t\t\tres.step();\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t\treturn driveIdArray;\n\t\t}\n\t}\n\t\n\t// Function to get the total number of rows in a table\n\tint getTotalRowCount() {\n\t\tsynchronized(databaseLock) {\n\t\t\tint rowCount = 0;\n\t\t\tauto stmt = db.prepare(\"SELECT COUNT(*) FROM item;\");\n\t\t\tscope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution.\n\n\t\t\ttry {\n\t\t\t\tauto res = stmt.exec();\n\t\t\t\tif (!res.empty) {\n\t\t\t\t\trowCount = res.front[0].to!int;\n\t\t\t\t}\n\t\t\t} catch (SqliteException exception) {\n\t\t\t\t// Handle the error appropriately\n\t\t\t\tdetailSQLErrorMessage(exception);\n\t\t\t}\n\t\t\t\n\t\t\treturn rowCount;\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/log.d",
    "content": "// What is this module called?\nmodule log;\n\n// What does this module require to function?\nimport std.stdio;\nimport std.file;\nimport std.datetime;\nimport std.concurrency;\nimport std.typecons;\nimport core.sync.mutex;\nimport core.sync.condition;\nimport core.thread;\nimport std.format;\nimport std.string;\nimport std.conv;\n\n// What other modules that we have created do we need to import?\nimport util;\n\nversion(Notifications) {\n\timport dnotify;\n}\n\n// Shared Application Logging Level Variables\n__gshared bool verboseLogging = false;\n__gshared bool debugLogging = false;\n__gshared bool debugHTTPSResponse = false;\n__gshared string microsoftDataCentre;\n\n// Private Shared Module Objects\nprivate __gshared LogBuffer logBuffer;\n// Timer for logging\nprivate __gshared MonoTime lastInsertedTime;\n// Is logging active\nprivate __gshared bool isRunning;\n\nclass LogBuffer {\n    \n\tprivate\tstring[3][] buffer;\n\tprivate Mutex bufferLock;\n\tprivate Condition condReady;\n\tprivate string logFilePath;\n\tprivate bool writeToFile;\n\tprivate bool verboseLogging;\n\tprivate bool debugLogging;\n\tprivate Thread flushThread;\n\tprivate bool environmentVariablesAvailable;\n\tprivate bool sendGUINotification;\n\t    \n\tthis(bool verboseLogging, bool debugLogging) {\n\t\t// Initialise the mutex\n\t\tbufferLock = new Mutex();\n\t\tcondReady = new Condition(bufferLock);\n\t\t// Initialise shared items\n\t\tisRunning = true;\n\t\t// Initialise other items\n\t\tthis.logFilePath = \"\";\n\t\tthis.writeToFile = false;\n\t\tthis.verboseLogging = verboseLogging;\n\t\tthis.debugLogging = debugLogging;\n\t\tthis.environmentVariablesAvailable = false;\n\t\tthis.sendGUINotification = false;\n\t\tthis.flushThread = new Thread(&flushBuffer);\n\t\tthis.flushThread.isDaemon(true);\n\t\tthis.flushThread.start();\n\t}\n\t\n\t~this() {\n\t\tif (!isRunning) {\t\t\n\t\t\tif (exitHandlerTriggered) {\n\t\t\t\tbufferLock.unlock();\t\n\t\t\t}\n\t\t}\n\t}\n\t\t\n\t// Terminate Logging\n\tvoid terminateLogging() {\n\t\tsynchronized {\n\t\t\t// join all threads\n\t\t\tthread_joinAll();\n\t\t\t\n\t\t\tif (!isRunning) {\n\t\t\t\treturn; // Prevent multiple shutdowns\n\t\t\t}\n\t\t\t\n\t\t\t// flag that we are no longer running due to shutting down\n\t\t\tisRunning = false;\n\t\t\tcondReady.notifyAll(); // Wake up all waiting threads\n\t\t}\n\n\t\t// Wait for the flush thread to finish outside of the synchronized block to avoid deadlocks\n\t\tif (flushThread.isRunning()) {\n\t\t\tflushThread.join(true);\n\t\t}\n\t\t\n\t\t// Flush any remaining logs\n\t\tflushBuffer();\n\t\t\n\t\t// Sleep for a while to avoid busy-waiting\n\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t\n\t\t// Exit scopes\n\t\tscope(exit) {\n\t\t\tif (bufferLock !is null) {\n\t\t\t\tbufferLock.lock();\n\t\t\t}\n\t\t\t\n\t\t\tscope(exit) {\n\t\t\t\tif (bufferLock !is null) {\n\t\t\t\t\tbufferLock.unlock();\n\t\t\t\t\tobject.destroy(bufferLock);\n\t\t\t\t\tbufferLock = null;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tscope(failure) {\n\t\t\tif (bufferLock !is null) {\n\t\t\t\tbufferLock.lock();\t\n\t\t\t}\n\t\t\t\n\t\t\tscope(exit) {\n\t\t\t\tif (bufferLock !is null) {\n\t\t\t\t\tbufferLock.unlock();\n\t\t\t\t\tobject.destroy(bufferLock);\n\t\t\t\t\tbufferLock = null;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Flush the logging buffer\n\tprivate void flushBuffer() {\n\t\twhile (isRunning) {\n\t\t\tflush();\n\t\t}\n\t\tstdout.flush();\n\t}\n\t\n\t// Add the message received to the buffer for logging\n\tvoid logThisMessage(string message, string[] levels = [\"info\"]) {\n\t\t// Generate the timestamp for this log entry\n\t\tauto timeStamp = leftJustify(Clock.currTime().toString(), 28, '0');\n\t\t\n\t\tsynchronized(bufferLock) {\n\t\t\tforeach (level; levels) {\n\t\t\t\t// Normal application output\n\t\t\t\tif (!debugLogging) {\n\t\t\t\t\tif ((level == \"info\") || ((verboseLogging) && (level == \"verbose\")) || (level == \"logFileOnly\") || (level == \"consoleOnly\") || (level == \"consoleOnlyNoNewLine\")) {\n\t\t\t\t\t\t// Add this message to the buffer, with this format\n\t\t\t\t\t\tbuffer ~= [timeStamp, level, format(\"%s\", message)];\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Debug Logging (--verbose --verbose | -v -v | -vv) output\n\t\t\t\t\t// Add this message, regardless of 'level' to the buffer, with this format\n\t\t\t\t\tbuffer ~= [timeStamp, level, format(\"DEBUG: %s\", message)];\n\t\t\t\t\t// If there are multiple 'levels' configured, ignore this and break as we are doing debug logging\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Submit the message to the dbus / notification daemon for display within the GUI being used\n\t\t\t\t// Will not send GUI notifications when running in debug mode\n\t\t\t\tif ((!debugLogging) && (level == \"notify\")) {\n\t\t\t\t\tif (sendGUINotification) {\n\t\t\t\t\t\tnotify(message);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Notify thread to wake up\n\t\t\tcondReady.notify();\n\t\t}\n\t}\n\t\t\n\t// Send GUI notification if --enable-notifications as been used at compile time\n\tvoid notify(string message) {\n\t\t// Use dnotify's functionality for GUI notifications, if GUI notification support has been compiled in\n\t\tversion(Notifications) {\n\t\t\ttry {\n\t\t\t\tauto n = new Notification(\"OneDrive Client for Linux\", message, \"dialog-information\");\n\t\t\t\tn.show();\n\t\t\t} catch (NotificationError e) {\n\t\t\t\taddLogEntry(\"Unable to send notification to the D-Bus message bus daemon, disabling GUI notifications: \" ~ e.message);\n\t\t\t\tsendGUINotification = false;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Flush the logging buffer\n\tprivate void flush() {\n\t\tstring[3][] messages;\n\t\tsynchronized(bufferLock) {\n\t\t\tif (isRunning) {\n\t\t\t\twhile (buffer.empty && isRunning) { // buffer is empty and logging is still active\n\t\t\t\t\tcondReady.wait();\n\t\t\t\t}\n\t\t\t\tmessages = buffer;\n\t\t\t\tbuffer.length = 0;\n\t\t\t}\n\t\t}\n\n\t\t// Are there messages to process?\n\t\tif (messages.length > 0) {\n\t\t\t// There are messages to process\n\t\t\tforeach (msg; messages) {\n\t\t\t\t// timestamp, logLevel, message\n\t\t\t\t// Always write the log line to the console, if level != logFileOnly\n\t\t\t\tif (msg[1] != \"logFileOnly\") {\n\t\t\t\t\t// Console output .. what sort of output\n\t\t\t\t\tif (msg[1] == \"consoleOnlyNoNewLine\") {\n\t\t\t\t\t\t// This is used write out a message to the console only, without a new line \n\t\t\t\t\t\t// 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\n\t\t\t\t\t\twrite(msg[2]);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// write this to the console with a new line\n\t\t\t\t\t\twriteln(msg[2]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Was this just console only output?\n\t\t\t\tif ((msg[1] != \"consoleOnlyNoNewLine\") && (msg[1] != \"consoleOnly\")) {\n\t\t\t\t\t// Write to the logfile only if configured to do so - console only items should not be written out\n\t\t\t\t\tif (writeToFile) {\n\t\t\t\t\t\tstring logFileLine = format(\"[%s] %s\", msg[0], msg[2]);\n\t\t\t\t\t\tstd.file.append(logFilePath, logFileLine ~ \"\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Clear Messages\n\t\t\tmessages.length = 0;\n\t\t}\n\t}\n}\n\n// Function to initialise the logging system\nvoid initialiseLogging(bool verboseLogging = false, bool debugLogging = false) {\n    logBuffer = new LogBuffer(verboseLogging, debugLogging);\n\tlastInsertedTime = MonoTime.currTime();\n}\n\n// Shutdown Logging\nvoid shutdownLogging() {\n\tif (logBuffer !is null) {\n\t\t// Terminate logging in a safe manner\n\t\tlogBuffer.terminateLogging();\n\t\tlogBuffer = null;\n\t}\n}\n\n// Function to add a log entry with multiple levels\nvoid addLogEntry(string message = \"\", string[] levels = [\"info\"]) {\n\t// we can only add a log line if we are running ... \n\tif (isRunning) {\n\t\tlogBuffer.logThisMessage(message, levels);\n\t}\n}\n\n// Is logging still active\nbool loggingActive() {\n\treturn isRunning;\n}\n\n// Is logging still initialised\nbool loggingStillInitialised() {\n\t return logBuffer !is null;\n}\n\nvoid addProcessingLogHeaderEntry(string message, long verbosityCount) {\n\tif (verbosityCount == 0) {\n\t\taddLogEntry(message, [\"logFileOnly\"]);\t\t\t\t\t\n\t\t// Use the dots to show the application is 'doing something' if verbosityCount == 0\n\t\taddLogEntry(message ~ \" .\", [\"consoleOnlyNoNewLine\"]);\n\t} else {\n\t\t// Fallback to normal logging if in verbose or above level\n\t\taddLogEntry(message);\n\t}\n}\n\n// Add a processing '.' to indicate activity\nvoid addProcessingDotEntry() {\n\tif (MonoTime.currTime() - lastInsertedTime < dur!\"seconds\"(1)) {\n\t\t// Don't flood the log buffer\n\t\treturn;\n\t}\n\tlastInsertedTime = MonoTime.currTime();\n\taddLogEntry(\".\", [\"consoleOnlyNoNewLine\"]);\n}\n\n// Finish processing '.' line output\nvoid completeProcessingDots() {\n\taddLogEntry(\" \", [\"consoleOnly\"]);\n}\n\n// Function to set logFilePath and enable logging to a file\nvoid enableLogFileOutput(string configuredLogFilePath) {\n\tlogBuffer.logFilePath = configuredLogFilePath;\n\tlogBuffer.writeToFile = true;\n}\n\n// Flag that the environment variables exists so if logging is compiled in, it can be enabled\nvoid flagEnvironmentVariablesAvailable(bool variablesAvailable) {\n\tlogBuffer.environmentVariablesAvailable = variablesAvailable;\n}\n\n// Disable GUI Notifications\nvoid disableGUINotifications(bool userConfigDisableNotifications) {\n\tlogBuffer.sendGUINotification = userConfigDisableNotifications;\n}\n\n// Validate that if GUI Notification support has been compiled in using --enable-notifications, the DBUS Server is actually usable\nvoid validateDBUSServerAvailability() {\n\tversion(Notifications) {\n\t\tif (logBuffer.environmentVariablesAvailable) {\n\t\t\tauto serverAvailable = dnotify.check_availability();\n\t\t\tif (!serverAvailable) {\n\t\t\t\taddLogEntry(\"WARNING: D-Bus message bus daemon is not available; GUI notifications are disabled\");\n\t\t\t\tlogBuffer.sendGUINotification = false;\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"D-Bus message bus daemon is available; GUI notifications are now enabled\");\n\t\t\t\tif (debugLogging) {addLogEntry(\"D-Bus message bus daemon server details: \" ~ to!string(dnotify.get_server_info()), [\"debug\"]);}\n\t\t\t\tlogBuffer.sendGUINotification = true;\n\t\t\t}\n\t\t} else {\n\t\t\taddLogEntry(\"WARNING: The required environment variables to enable GUI Notifications are not available; GUI notifications are disabled\");\n\t\t\tlogBuffer.sendGUINotification = false;\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/main.d",
    "content": "// What is this module called?\nmodule main;\n\n// What does this module require to function?\nimport core.memory;\nimport core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;\nimport core.sys.posix.signal;\nimport core.thread;\nimport core.time;\nimport std.algorithm;\nimport std.concurrency;\nimport std.conv;\nimport std.datetime;\nimport std.file;\nimport std.getopt;\nimport std.net.curl: CurlException;\nimport std.parallelism;\nimport std.path;\nimport std.process;\nimport std.socket: SocketException;\nimport std.stdio;\nimport std.string;\nimport std.traits;\n\n// What other modules that we have created do we need to import?\nimport config;\nimport log;\nimport curlEngine;\nimport util;\nimport onedrive;\nimport syncEngine;\nimport itemdb;\nimport clientSideFiltering;\nimport monitor;\nimport webhook;\nimport intune;\nimport socketio;\n\n// What other constant variables do we require?\nconst int EXIT_RESYNC_REQUIRED = 126;\n\n// Class objects\nApplicationConfig appConfig;\nOneDriveWebhook oneDriveWebhook;\nSyncEngine syncEngineInstance;\nItemDatabase itemDB;\nClientSideFiltering selectiveSync;\nMonitor filesystemMonitor;\nOneDriveSocketIo oneDriveSocketIo;\n\n// Class variables\n// Flag for performing a synchronised shutdown\nbool shutdownInProgress = false;\n// Flag if a --dry-run is being performed, as, on shutdown, once config is destroyed, we have no reference here\nbool dryRun = false;\n// Configure the runtime database file path so that it is available to us on shutdown so objects can be destroyed and removed if required\n// - Typically this will be the default, but in a --dry-run scenario, we use a separate database file\nstring runtimeDatabaseFile = \"\";\n// Flag for if we are performing filesystem monitoring \nbool performFileSystemMonitoring = false;\n// Flag for if we perform a database vacuum. This gets set to false if we have not performed a 'no-sync' task\nbool performDatabaseVacuum = true;\n// Flag if SIGTERM is used\nbool sigtermHandlerTriggered = false;\n\nint main(string[] cliArgs) {\n\t// Application Start Time - used during monitor loop to detail how long it has been running for\n\tauto applicationStartTime = Clock.currTime();\n\t// Disable buffering on stdout - this is needed so that when we are using plain write() it will go to the terminal without flushing\n\tstdout.setvbuf(0, _IONBF);\n\t\n\t// Required main function variables\n\tstring genericHelpMessage = \"Please use 'onedrive --help' for further assistance in regards to running this application.\";\n\t// If the user passes in --confdir we need to store this as a variable\n\tstring confdirOption = \"\";\n\t// running as what user?\n\tstring runtimeUserName = \"\";\n\t// Are we online?\n\tbool online = false;\n\t// Does the operating environment have shell environment variables set\n\tbool shellEnvSet = false;\n\t// What is the runtime synchronisation directory that will be used\n\t// Typically this will be '~/OneDrive' .. however tilde expansion is unreliable\n\tstring runtimeSyncDirectory = \"\";\n\t// Verbosity Logging Count - this defines if verbose or debug logging is being used\n\tlong verbosityCount = 0;\n\t// Monitor loop failures\n\tbool monitorFailures = false;\n\t// Help requested\n\tbool helpRequested = false;\n\t// Did the user specify --sync or --monitor\n\tbool syncOrMonitorMissing = false;\n\t// Was a no-sync type operation requested\n\tbool noSyncTaskOperationRequested = false;\n\t\n\t// DEVELOPER OPTIONS OUTPUT VARIABLES\n\tbool displayMemoryUsage = false;\n\tbool displaySyncOptions = false;\n\t\n\t// Application Version\n\timmutable string applicationVersion = \"onedrive \" ~ strip(import(\"version\"));\n\t\n\t// Define 'exit' and 'failure' scopes\n\tscope(exit) {\n\t\t// Detail what scope was called\n\t\tif (debugLogging) {addLogEntry(\"Exit scope was called\", [\"debug\"]);}\n\t\t// Perform synchronised exit\n\t\tperformSynchronisedExitProcess(\"exitScope\");\n\t\t// Setup signal handling for the exit scope\n\t\tsetupExitScopeSignalHandler();\n\t}\n\t\n\tscope(failure) {\n\t\t// Detail what scope was called\n\t\tif (debugLogging) {addLogEntry(\"Failure scope was called\", [\"debug\"]);}\n\t\t// Perform synchronised exit\n\t\tperformSynchronisedExitProcess(\"failureScope\");\n\t\t// Setup signal handling for the exit scope\n\t\tsetupExitScopeSignalHandler();\n\t}\n\t\n\t// Read in application options as passed in\n\ttry {\n\t\tbool printVersion = false;\n\t\tauto cliOptions = getopt(\n\t\t\tcliArgs,\n\t\t\tstd.getopt.config.passThrough,\n\t\t\tstd.getopt.config.bundling,\n\t\t\tstd.getopt.config.caseSensitive,\n\t\t\t\"confdir\", \"Set the directory used to store the configuration files\", &confdirOption,\n\t\t\t\"verbose|v+\", \"Print more details, useful for debugging (repeat for extra debugging)\", &verbosityCount,\n\t\t\t\"version\", \"Print the version and exit\", &printVersion\n\t\t);\n\t\t\n\t\t// Print help and exit\n\t\tif (cliOptions.helpWanted) {\n\t\t\tcliArgs ~= \"--help\";\n\t\t\thelpRequested = true;\n\t\t}\n\t\t// Print the version and exit\n\t\tif (printVersion) {\n\t\t\twriteln(applicationVersion);\n\t\t\texit(EXIT_SUCCESS);\n\t\t}\n\t} catch (GetOptException e) {\n\t\t// Option errors\n\t\twriteln(e.msg);\n\t\twriteln(genericHelpMessage);\n\t\treturn EXIT_FAILURE;\n\t} catch (Exception e) {\n\t\t// Generic error\n\t\twriteln(e.msg);\n\t\twriteln(genericHelpMessage);\n\t\treturn EXIT_FAILURE;\n\t}\n\t\n\t// Determine the application logging verbosity\n\t// - 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\n\tif (verbosityCount == 1) { verboseLogging = true;} // set __gshared bool verboseLogging in log.d\n\tif (verbosityCount >= 2) { verboseLogging = true; debugLogging = true;}   // set __gshared bool verboseLogging & debugLogging in log.d\n\t\n\t// Initialize the application logging class, as we know the application verbosity level\n\t// 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\n    initialiseLogging(verboseLogging, debugLogging);\n\t\n\t// Log application start time, log line has start time\n\tif (debugLogging) {addLogEntry(\"Application started\", [\"debug\"]);}\n\t\n\t// Who are we running as? This will print the ProcessID, UID, GID and username the application is running as\n\truntimeUserName = getUserName();\n\t\n\t// Print the application version and how this was compiled as soon as possible\n\tif (debugLogging) {\n\t\taddLogEntry(\"Application Version: \" ~ applicationVersion, [\"debug\"]);\n\t\taddLogEntry(\"Application Compiled With: \" ~ compilerDetails(), [\"debug\"]);\n\t\n\t\t// How was this application started - what options were passed in\n\t\taddLogEntry(\"Passed in 'cliArgs': \" ~ to!string(cliArgs), [\"debug\"]);\n\t\taddLogEntry(\"Note: --confdir and --verbose are not listed in 'cliArgs' array\", [\"debug\"]);\n\t\taddLogEntry(\"Passed in --confdir if present: \" ~ confdirOption, [\"debug\"]);\n\t\taddLogEntry(\"Passed in --verbose count if present: \" ~ to!string(verbosityCount), [\"debug\"]);\n\t}\n\t\n\t// Create a new AppConfig object with default values, \n\tappConfig = new ApplicationConfig();\n\t// Update the default application configuration with the verbosity count so this can be used throughout the application as needed\n\tappConfig.verbosityCount = verbosityCount;\n\t\n\t// Initialise the application configuration, utilising --confdir if it was passed in\n\t// Otherwise application defaults will be used to configure the application\n\tif (!appConfig.initialise(confdirOption, helpRequested)) {\n\t\t// There was an error loading the user specified application configuration\n\t\t// Error message already printed\n\t\treturn EXIT_FAILURE;\n\t}\n\t\n\t// Update the current runtime application configuration (default or 'config' file read in options) from any passed in command line arguments\n\tappConfig.updateFromArgs(cliArgs);\n\t\n\t// Set the default thread pool value based on configuration or maximum logical CPUs\n\tsetDefaultApplicationThreads();\n\t\n\t// If --debug-https has been used, set the applicable flag\n\tdebugHTTPSResponse = appConfig.getValueBool(\"debug_https\"); // set __gshared bool debugHTTPSResponse in log.d now that we have read-in any CLI arguments\n\t\n\t// Read in the configured 'sync_dir' from appConfig with '~' if present correctly expanded based on the user environment\n\truntimeSyncDirectory = appConfig.initialiseRuntimeSyncDirectory();\n\t\n\t// Are we doing a --sync or a --monitor operation? Both of these will be false if they are not set\n\tif ((!appConfig.getValueBool(\"synchronize\")) && (!appConfig.getValueBool(\"monitor\"))) {\n\t\tsyncOrMonitorMissing = true; // --sync or --monitor is missing \n\t}\n\t\n\t// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session\n\t// This is ONLY possible on Linux, not FreeBSD or other platforms\n\tversion (linux) {\n\t\tif (appConfig.getValueBool(\"use_intune_sso\")) {\n\t\t\t// The client is configured to use Intune SSO via Microsoft Identity Broker dbus session\n\t\t\taddLogEntry(\"Client has been configured to use Intune SSO via Microsoft Identity Broker dbus session - checking usage criteria\");\n\t\t\t// We need to check that the available dbus is actually available\n\t\t\tif(wait_for_broker()) {\n\t\t\t\t// Usage criteria met, will attempt to use Intune SSO via dbus\n\t\t\t\taddLogEntry(\"Intune SSO via Microsoft Identity Broker dbus session usage criteria met - will attempt to authenticate via Intune\");\n\t\t\t} else {\n\t\t\t\t// Microsoft Identity Broker dbus is not available\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"Required Microsoft Identity Broker dbus capability not found - disabling authentication via Intune SSO\");\n\t\t\t\taddLogEntry();\n\t\t\t\tappConfig.setValueBool(\"use_intune_sso\" , false);\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Ensure 'use_intune_sso' is disabled\n\t\tappConfig.setValueBool(\"use_intune_sso\" , false);\n\t}\n\t\n\t// Has the user configured to use the 'Recycle Bin' locally, for any files that are deleted online?\n\tif (appConfig.getValueBool(\"use_recycle_bin\")) {\n\t\t// Configure the internal application paths which will be used to move rather than delete any online deletes to\n\t\tappConfig.setRecycleBinPaths();\n\t\t\n\t\t// If we are not using --display-config, test if the Recycle Bin Paths exist on the file system\n\t\tif (!appConfig.getValueBool(\"display_config\")) {\n\t\t\t\n\t\t\t// We need to test that the configured 'Recycle Bin' path is not within the configured 'sync_dir'\n\t\t\tif (appConfig.checkRecycleBinPathAsChildOfSyncDir) {\n\t\t\t\t// ERROR: 'Recycle Bin' path is a child of the configured 'sync_dir'\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: The configured 'recycle_bin_path' (\" ~ appConfig.recycleBinParentPath ~ \") is located within the configured 'sync_dir' (\" ~ appConfig.runtimeSyncDirectory ~ \").\", [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry(\"       This would cause locally recycled items to be re-uploaded to Microsoft OneDrive.\");\n\t\t\t\taddLogEntry(\"       Please set 'recycle_bin_path' to a location outside of 'sync_dir' and restart the client.\");\n\t\t\t\taddLogEntry();\n\t\t\t\treturn EXIT_FAILURE;\n\t\t\t} else {\n\t\t\t\t// 'Recycle Bin' path is not within the configured 'sync_dir'\n\t\t\t\t// We need to ensure that the Recycle Bin Paths exist on the file system, and if they do not exist, create them\n\t\t\t\t// Test for appConfig.recycleBinFilePath\n\t\t\t\tif (!exists(appConfig.recycleBinFilePath)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Attempt to create the 'Recycle Bin' file path we have been configured with\n\t\t\t\t\t\tmkdirRecurse(appConfig.recycleBinFilePath);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Configure the applicable permissions for the folder\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting directory permissions for: \" ~ appConfig.recycleBinFilePath, [\"debug\"]);}\n\t\t\t\t\t\tappConfig.recycleBinFilePath.setAttributes(octal!700); // Set to 0700 as Trash may contain sensitive and is the expected default permissions by GIO or KIO\n\t\t\t\t\t\t\n\t\t\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t\t\t// Creating the 'Recycle Bin' file path failed\n\t\t\t\t\t\taddLogEntry(\"ERROR: Unable to create the configured local 'Recycle Bin' file directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\t\treturn EXIT_FAILURE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Test for appConfig.recycleBinInfoPath\n\t\t\t\tif (!exists(appConfig.recycleBinInfoPath)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Attempt to create the 'Recycle Bin' info path we have been configured with\n\t\t\t\t\t\tmkdirRecurse(appConfig.recycleBinInfoPath);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Configure the applicable permissions for the folder\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting directory permissions for: \" ~ appConfig.recycleBinInfoPath, [\"debug\"]);}\n\t\t\t\t\t\tappConfig.recycleBinInfoPath.setAttributes(octal!700); // Set to 0700 as Trash may contain sensitive and is the expected default permissions by GIO or KIO\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t\t\t// Creating the 'Recycle Bin' info path failed\n\t\t\t\t\t\taddLogEntry(\"ERROR: Unable to create the configured local 'Recycle Bin' info directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\t\treturn EXIT_FAILURE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Are we performing some sort of 'no-sync' operation task?\n\tnoSyncTaskOperationRequested = appConfig.hasNoSyncOperationBeenRequested(); // returns true if we are\n\t\n\t// 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'\n\t// - 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\n\t\n\t// If we are not using --display-config, perform this check\n\tif (!appConfig.getValueBool(\"display_config\")) {\n\t\tif (syncOrMonitorMissing && !noSyncTaskOperationRequested) {\n\t\t\t// Before failing fast, has the client been authenticated and does the 'refresh_token' contain data\n\t\t\tif (exists(appConfig.refreshTokenFilePath) && getSize(appConfig.refreshTokenFilePath) > 0) {\n\t\t\t\t// fail fast - print error message that --sync or --monitor are missing\n\t\t\t\tprintMissingOperationalSwitchesError();\n\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\treturn EXIT_FAILURE;\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// If --disable-notifications has not been used, check if everything exists to enable notifications\n\tif (!appConfig.getValueBool(\"disable_notifications\")) {\n\t\t// If notifications was compiled in, we need to ensure that these variables are actually available before we enable GUI Notifications\n\t\tflagEnvironmentVariablesAvailable(appConfig.validateGUINotificationEnvironmentVariables());\n\t\t// If we are not using --display-config attempt to enable GUI notifications\n\t\tif (!appConfig.getValueBool(\"display_config\")) {\n\t\t\t// Attempt to enable GUI Notifications\n\t\t\tvalidateDBUSServerAvailability();\n\t\t}\n\t}\n\t\n\t// cURL Version Compatibility Test\n\t// - Common warning for cURL version issue\n\tstring distributionWarning = \"         Please report this to your distribution, requesting an update to a newer cURL version, or consider upgrading it yourself for optimal stability.\";\n\t// If 'force_http_11' = false, we need to check the curl version being used\n\tif (!appConfig.getValueBool(\"force_http_11\")) {\n\t\t// get the curl version\n\t\tstring curlVersion = getCurlVersionNumeric();\n\t\t\n\t\t// Is the version of curl or libcurl being used by the platform a known bad curl version for HTTP/2 support\n\t\tif (isBadCurlVersion(curlVersion)) {\n\t\t\t// add warning message\n\t\t\tstring curlWarningMessage = format(\"WARNING: Your cURL/libcurl version (%s) has known HTTP/2 bugs that impact the use of this client.\", curlVersion);\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(curlWarningMessage, [\"info\", \"notify\"]);\n\t\t\taddLogEntry(distributionWarning);\n\t\t\taddLogEntry(\"         Downgrading all client operations to use HTTP/1.1 to ensure maximum operational stability.\");\n\t\t\taddLogEntry(\"         Please read https://github.com/abraunegg/onedrive/blob/master/docs/usage.md#compatibility-with-curl for more information.\");\n\t\t\taddLogEntry();\n\t\t\tappConfig.setValueBool(\"force_http_11\" , true);\n\t\t}\n\t} else {\n\t\t// get the curl version - a bad curl version may still be in use\n\t\tstring curlVersion = getCurlVersionNumeric();\n\t\t\n\t\t// Is the version of curl or libcurl being used by the platform a known bad curl version\n\t\tif (isBadCurlVersion(curlVersion)) {\n\t\t\t// add warning message\n\t\t\tstring curlWarningMessage = format(\"WARNING: Your cURL/libcurl version (%s) has known operational bugs that impact the use of this client.\", curlVersion);\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(curlWarningMessage); // curl HTTP/1.1 downgrade in place meaning user took steps to remediate, perform standard logging with no GUI notification\n\t\t\taddLogEntry(distributionWarning);\n\t\t\taddLogEntry();\n\t\t}\n\t}\n\t\n\t// In a debug scenario, to assist with understanding the run-time configuration, ensure this flag is set\n\tif (debugLogging) {\n\t\tappConfig.setValueBool(\"display_running_config\", true);\n\t}\n\t\n\t// Configure dryRun so that this can be used here & during shutdown\n\tdryRun = appConfig.getValueBool(\"dry_run\");\n\t\n\t// 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:\n\t// - Enable logging to a file if this is required\n\t// - Disable GUI notifications if this has been configured\n\t\n\t// Configure application logging to a log file only if this has been enabled\n\t// 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.\n\t// 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.\n\t// The buffered logging also ensures that all 'output' to this point is also captured and written out to the log file\n\tif (appConfig.getValueBool(\"enable_logging\")) {\n\t\t// Calculate the application logging directory\n\t\tstring calculatedLogDirPath = appConfig.calculateLogDirectory();\n\t\tstring calculatedLogFilePath;\n\t\t// Initialise using the configured logging directory\n\t\tif (verboseLogging) {addLogEntry(\"Using the following path to store the runtime application log: \" ~ calculatedLogDirPath, [\"verbose\"]);}\n\t\t// Calculate the logfile name\n\t\tif (calculatedLogDirPath != appConfig.defaultHomePath) {\n\t\t\t// Log file is not going to the home directory\n\t\t\tstring logfileName = runtimeUserName ~ \".onedrive.log\";\n\t\t\tcalculatedLogFilePath = buildNormalizedPath(buildPath(calculatedLogDirPath, logfileName));\n\t\t} else {\n\t\t\t// Log file is going to the users home directory\n\t\t\tcalculatedLogFilePath = buildNormalizedPath(buildPath(calculatedLogDirPath, \"onedrive.log\"));\n\t\t}\n\t\t// Update the logging class to use 'calculatedLogFilePath' for the application log file now that this has been determined\n\t\tenableLogFileOutput(calculatedLogFilePath);\n\t}\n\t\n\t// Disable GUI Notifications if configured to do so\n\t// - This option is reverse action. If 'disable_notifications' is 'true', we need to send 'false'\n\tif (appConfig.getValueBool(\"disable_notifications\")) {\n\t\t// disable_notifications is true, ensure GUI notifications is initialised with false so that NO GUI notification is sent\n\t\tdisableGUINotifications(false);\n\t\taddLogEntry(\"Disabling GUI notifications as per user configuration\");\n\t}\n\t\n\t// 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\n\tappConfig.checkDeprecatedOptions(cliArgs);\n\t\n\t// Configure Client Side Filtering (selective sync) by parsing and getting a usable regex for skip_file, skip_dir and sync_list config components\n\tselectiveSync = new ClientSideFiltering(appConfig);\n\tif (!selectiveSync.initialise()) {\n\t\t// exit here as something triggered a selective sync configuration failure\n\t\treturn EXIT_FAILURE;\n\t}\n\t\n\t// Set runtimeDatabaseFile, this will get updated if we are using --dry-run\n\truntimeDatabaseFile = appConfig.databaseFilePath;\n\t\n\t// DEVELOPER OPTIONS OUTPUT\n\t// Set to display memory details as early as possible\n\tdisplayMemoryUsage = appConfig.getValueBool(\"display_memory\");\n\t// set to display sync options\n\tdisplaySyncOptions = appConfig.getValueBool(\"display_sync_options\");\n\t\n\t// 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\n\tif ((appConfig.getValueBool(\"display_config\")) || (appConfig.getValueBool(\"display_running_config\"))) {\n\t\t// Display the application configuration\n\t\tappConfig.displayApplicationConfiguration();\n\t\t// Do we exit? We exit only if '--display-config' has been used\n\t\tif (appConfig.getValueBool(\"display_config\")) {\n\t\t\treturn EXIT_SUCCESS;\n\t\t}\n\t}\n\t\n\t// 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\n\tif (appConfig.checkForBasicOptionConflicts) {\n\t\t// 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\n\t\treturn EXIT_FAILURE;\n\t}\n\t\n\t// Check for --dry-run operation or a 'no-sync' operation where the 'dry-run' DB copy should be used\n\t// If this has been requested, we need to ensure that all actions are performed against the dry-run database copy, and, \n\t// 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\n\tif (dryRun || (noSyncTaskOperationRequested)) {\n\t\t// Cleanup any existing dry-run elements ... these should never be left hanging around and should be cleaned up first\n\t\tcleanupDatabaseFiles(appConfig.databaseFilePathDryRun);\n\t\t\n\t\t// If --dry-run\n\t\tif (dryRun) {\n\t\t\t// This is a --dry-run operation\n\t\t\taddLogEntry(\"DRY-RUN Configured. Output below shows what 'would' have occurred.\");\n\t\t \n\t\t\t// Make a copy of the original items.sqlite3 for use as the dry run copy if it exists\n\t\t\tif (exists(appConfig.databaseFilePath)) {\n\t\t\t\t// In a --dry-run --resync scenario, we should not copy the existing database file\n\t\t\t\tif (!appConfig.getValueBool(\"resync\")) {\n\t\t\t\t\t// Copy the existing DB file to the dry-run copy\n\t\t\t\t\taddLogEntry(\"DRY-RUN: Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations\");\n\t\t\t\t\tcopy(appConfig.databaseFilePath,appConfig.databaseFilePathDryRun);\n\t\t\t\t} else {\n\t\t\t\t\t// No database copy due to --resync - an empty DB file will be used for the resync operation\n\t\t\t\t\taddLogEntry(\"DRY-RUN: No database copy created for --dry-run due to --resync also being used\");\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// update runtimeDatabaseFile now that we are using the dry run path\n\t\t\truntimeDatabaseFile = appConfig.databaseFilePathDryRun;\n\t\t}\n\t} else {\n\t\t// Cleanup any existing dry-run elements ... these should never be left hanging around\n\t\tcleanupDatabaseFiles(appConfig.databaseFilePathDryRun);\n\t}\n\t\n\t// Handle --logout as separate item, do not 'resync' on a --logout\n\tif (appConfig.getValueBool(\"logout\")) {\n\t\tif (debugLogging) {addLogEntry(\"--logout requested\", [\"debug\"]);}\n\t\taddLogEntry(\"Deleting the saved authentication status ...\");\n\t\tif (!dryRun) {\n\t\t\t// Remove the 'refresh_token' file if present\n\t\t\tsafeRemove(appConfig.refreshTokenFilePath);\n\t\t\t// Remove the 'intune_account' file if present\n\t\t\tsafeRemove(appConfig.intuneAccountDetailsFilePath);\n\t\t} else {\n\t\t\t// --dry-run scenario ... technically we should not be making any local file changes .......\n\t\t\taddLogEntry(\"DRY-RUN: Not removing the saved authentication status\");\n\t\t}\n\t\t// Exit\n\t\treturn EXIT_SUCCESS;\n\t}\n\t\n\t// Handle --reauth to re-authenticate the client\n\tif (appConfig.getValueBool(\"reauth\")) {\n\t\tif (debugLogging) {addLogEntry(\"--reauth requested\", [\"debug\"]);}\n\t\taddLogEntry(\"Deleting the saved authentication status ... re-authentication requested\");\n\t\tif (!dryRun) {\n\t\t\t// Remove the 'refresh_token' file if present\n\t\t\tsafeRemove(appConfig.refreshTokenFilePath);\n\t\t\t// Remove the 'intune_account' file if present\n\t\t\tsafeRemove(appConfig.intuneAccountDetailsFilePath);\n\t\t} else {\n\t\t\t// --dry-run scenario ... technically we should not be making any local file changes .......\n\t\t\taddLogEntry(\"DRY-RUN: Not removing the saved authentication status\");\n\t\t}\n\t}\n\t\n\t// --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\n\t// 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\n\tif (appConfig.getValueBool(\"resync\")) {\n\t\t// what is the risk acceptance for --resync?\n\t\tbool resyncRiskAcceptance = appConfig.displayResyncRiskForAcceptance();\n\t\tif (debugLogging) {addLogEntry(\"Returned --resync risk acceptance: \" ~ to!string(resyncRiskAcceptance), [\"debug\"]);}\n\t\t\n\t\t// Action based on user response\n\t\tif (!resyncRiskAcceptance){\n\t\t\t// --resync risk not accepted\n\t\t\treturn EXIT_FAILURE;\n\t\t} else {\n\t\t\tif (debugLogging) {addLogEntry(\"--resync issued and risk accepted\", [\"debug\"]);}\n\t\t\t// --resync risk accepted, perform a cleanup of items that require a cleanup\n\t\t\tappConfig.cleanupHashFilesDueToResync();\n\t\t\t// Make a backup of the applicable configuration file\n\t\t\tappConfig.createBackupConfigFile();\n\t\t\t// Update hash files and generate a new config backup\n\t\t\tappConfig.updateHashContentsForConfigFiles();\n\t\t\t// Remove the items database\n\t\t\tprocessResyncDatabaseRemoval(runtimeDatabaseFile);\n\t\t}\n\t} else {\n\t\t// Is the application currently authenticated? If not, it is pointless checking if a --resync is required until the application is authenticated\n\t\tif (exists(appConfig.refreshTokenFilePath)) {\n\t\t\t// Has any of our application configuration that would require a --resync been changed?\n\t\t\tif (appConfig.applicationChangeWhereResyncRequired()) {\n\t\t\t\t// Application configuration has changed however --resync not issued, fail fast\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"An application configuration change has been detected where a --resync is required\", [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry();\n\t\t\t\treturn EXIT_RESYNC_REQUIRED;\n\t\t\t} else {\n\t\t\t\t// No configuration change that requires a --resync to be issued\n\t\t\t\t// Special cases need to be checked - if these options were enabled, it creates a false 'Resync Required' flag, so do not create a backup\n\t\t\t\tif ((!appConfig.getValueBool(\"list_business_shared_items\"))) {\n\t\t\t\t\t// Make a backup of the applicable configuration file\n\t\t\t\t\tappConfig.createBackupConfigFile();\n\t\t\t\t\t// Update hash files and generate a new config backup\n\t\t\t\t\tappConfig.updateHashContentsForConfigFiles();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Implement https://github.com/abraunegg/onedrive/issues/1129\n\t// Force a synchronisation of a specific folder, only when using --synchronize --single-directory and ignoring all non-default skip_dir and skip_file rules\n\tif (appConfig.getValueBool(\"force_sync\")) {\n\t\t// appConfig.checkForBasicOptionConflicts() has already checked for the basic requirements for --force-sync\n\t\taddLogEntry();\n\t\taddLogEntry(\"WARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --sync --single-directory --force-sync being used\");\n\t\taddLogEntry();\n\t\tbool forceSyncRiskAcceptance = appConfig.displayForceSyncRiskForAcceptance();\n\t\tif (debugLogging) {addLogEntry(\"Returned --force-sync risk acceptance: \" ~ forceSyncRiskAcceptance, [\"debug\"]);}\n\t\t\n\t\t// Action based on user response\n\t\tif (!forceSyncRiskAcceptance){\n\t\t\t// --force-sync risk not accepted\n\t\t\treturn EXIT_FAILURE;\n\t\t} else {\n\t\t\t// --force-sync risk accepted\n\t\t\t// reset set config using function to use application defaults\n\t\t\tappConfig.resetSkipToDefaults();\n\t\t\t// update sync engine regex with reset defaults\n\t\t\tselectiveSync.setDirMask(appConfig.getValueString(\"skip_dir\"));\n\t\t\tselectiveSync.setFileMask(appConfig.getValueString(\"skip_file\"));\t\n\t\t}\n\t}\n\t\n\t// What IP Protocol are we going to use to access the network with\n\tappConfig.displayIPProtocol();\n\t\n\t// Test if OneDrive service can be reached, exit if it cant be reached\n\tif (debugLogging) {addLogEntry(\"Testing network to ensure network connectivity to Microsoft OneDrive Service\", [\"debug\"]);}\n\tonline = testInternetReachability(appConfig);\n\t\n\t// If we are not 'online' - how do we handle this situation?\n\tif (!online) {\n\t\t// We are unable to initialise the OneDrive API as we are not online\n\t\tif (!appConfig.getValueBool(\"monitor\")) {\n\t\t\t// Running as --synchronize\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"ERROR: Unable to reach the Microsoft OneDrive API service, unable to initialise application\");\n\t\t\taddLogEntry();\n\t\t\treturn EXIT_FAILURE;\n\t\t} else {\n\t\t\t// Running as --monitor\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"Unable to reach the Microsoft OneDrive API service at this point in time, re-trying network tests based on applicable intervals\");\n\t\t\taddLogEntry();\n\t\t\t// Run the re-try of Internet connectivity test\n\t\t\tonline = retryInternetConnectivityTest(appConfig);\n\t\t}\n\t}\n\t\n\t// 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\n\tif (online) {\n\t\t// Check Application Version\n\t\tif (!appConfig.getValueBool(\"disable_version_check\")) {\n\t\t\tif (verboseLogging) {addLogEntry(\"Checking Application Version ...\", [\"verbose\"]);}\n\t\t\tcheckApplicationVersion();\n\t\t}\n\t\t\n\t\t// Initialise the OneDrive API\n\t\tif (verboseLogging) {addLogEntry(\"Attempting to initialise the OneDrive API ...\", [\"verbose\"]);}\n\t\tOneDriveApi oneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tappConfig.apiWasInitialised = oneDriveApiInstance.initialise();\n\t\t\n\t\t// Did the API initialise successfully?\n\t\tif (appConfig.apiWasInitialised) {\n\t\t\tif (verboseLogging) {addLogEntry(\"The OneDrive API was initialised successfully\", [\"verbose\"]);}\n\t\t\t\n\t\t\t// Flag that we were able to initialise the API in the application config\n\t\t\toneDriveApiInstance.debugOutputConfiguredAPIItems();\n\t\t\toneDriveApiInstance.releaseCurlEngine();\n\t\t\tobject.destroy(oneDriveApiInstance);\n\t\t\toneDriveApiInstance = null;\n\t\t\t\n\t\t\t// Need to configure the itemDB and syncEngineInstance for 'sync' and 'non-sync' operations\n\t\t\tif (verboseLogging) {addLogEntry(\"Opening the item database ...\", [\"verbose\"]);}\n\t\t\t\n\t\t\t// Configure the Item Database\n\t\t\titemDB = new ItemDatabase(runtimeDatabaseFile);\n\t\t\t// Was the database successfully initialised?\n\t\t\tif (!itemDB.isDatabaseInitialised()) {\n\t\t\t\t// no .. destroy class\n\t\t\t\titemDB = null;\n\t\t\t\t// exit application\n\t\t\t\treturn EXIT_FAILURE;\n\t\t\t}\n\t\t\t\n\t\t\t// Initialise the syncEngine\n\t\t\tsyncEngineInstance = new SyncEngine(appConfig, itemDB, selectiveSync);\n\t\t\tappConfig.syncEngineWasInitialised = syncEngineInstance.initialise();\n\t\t\t\n\t\t\t// Are we not doing a --sync or a --monitor operation?\n\t\t\tif (syncOrMonitorMissing) { // this is 'true' if --sync or a --monitor were not used\n\n\t\t\t\t// Do not perform a vacuum on exit, pointless\n\t\t\t\tperformDatabaseVacuum = false;\n\t\t\t\t\n\t\t\t\t// Are we performing some sort of 'no-sync' task?\n\t\t\t\t// - Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library?\n\t\t\t\t// - Are we displaying the sync status?\n\t\t\t\t// - Are we getting the URL for a file online?\n\t\t\t\t// - Are we listing who modified a file last online?\n\t\t\t\t// - Are we listing OneDrive Business Shared Items?\n\t\t\t\t// - Are we creating a shareable link for an existing file on OneDrive?\n\t\t\t\t// - Are we just creating a directory online, without any sync being performed?\n\t\t\t\t// - Are we just deleting a directory online, without any sync being performed?\n\t\t\t\t// - Are we renaming or moving a directory?\n\t\t\t\t// - Are we displaying the quota information?\n\t\t\t\t// - Did we just authorise the client?\n\n\t\t\t\t// --get-sharepoint-drive-id - Get the SharePoint Library drive_id\n\t\t\t\tif (appConfig.getValueString(\"sharepoint_library_name\") != \"\") {\n\t\t\t\t\t// Get the SharePoint Library drive_id\n\t\t\t\t\tsyncEngineInstance.querySiteCollectionForDriveID(appConfig.getValueString(\"sharepoint_library_name\"));\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API and cleanup data\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\n\t\t\t\t// --display-sync-status - Query the sync status\n\t\t\t\tif (appConfig.getValueBool(\"display_sync_status\")) {\n\t\t\t\t\t// path to query variable\n\t\t\t\t\tstring pathToQueryStatusOn;\n\t\t\t\t\t// What path do we query?\n\t\t\t\t\tif (!appConfig.getValueString(\"single_directory\").empty) {\n\t\t\t\t\t\tpathToQueryStatusOn = \"/\" ~ appConfig.getValueString(\"single_directory\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpathToQueryStatusOn = \"/\";\n\t\t\t\t\t}\n\t\t\t\t\t// Query the sync status\n\t\t\t\t\tsyncEngineInstance.queryOneDriveForSyncStatus(pathToQueryStatusOn);\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API and cleanup data\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\n\t\t\t\t// --get-file-link - Get the URL path for a synced file\n\t\t\t\tif (appConfig.getValueString(\"get_file_link\") != \"\") {\n\t\t\t\t\t// Query the OneDrive API for the file link\n\t\t\t\t\tsyncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString(\"get_file_link\"), runtimeSyncDirectory, \"URL\");\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API and cleanup data\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// --modified-by - Get listing the modified-by details of a provided path\n\t\t\t\tif (appConfig.getValueString(\"modified_by\") != \"\") {\n\t\t\t\t\t// Query the OneDrive API for the last modified by details\n\t\t\t\t\tsyncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString(\"modified_by\"), runtimeSyncDirectory, \"ModifiedBy\");\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API and cleanup data\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// --list-shared-items - Get listing OneDrive Business Shared Items\n\t\t\t\tif (appConfig.getValueBool(\"list_business_shared_items\")) {\n\t\t\t\t\t// Is this a business account type?\n\t\t\t\t\tif (appConfig.accountType == \"business\") {\n\t\t\t\t\t\t// List OneDrive Business Shared Items\n\t\t\t\t\t\tsyncEngineInstance.listBusinessSharedObjects();\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"ERROR: Unsupported account type for listing OneDrive Business Shared Items\");\n\t\t\t\t\t}\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// --create-share-link - Create a shareable link for an existing file, based on the local path\n\t\t\t\tif (appConfig.getValueString(\"create_share_link\") != \"\") {\n\t\t\t\t\t// Query OneDrive for the file, and if valid, create a shareable link for the file\n\t\t\t\t\t\n\t\t\t\t\t// By default, the shareable link will be read-only. \n\t\t\t\t\t// If the user adds: \n\t\t\t\t\t//\t\t--with-editing-perms \n\t\t\t\t\t// this will create a writeable link\n\t\t\t\t\tsyncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString(\"create_share_link\"), runtimeSyncDirectory, \"ShareableLink\");\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// --create-directory - Are we just creating a directory online, without any sync being performed?\n\t\t\t\tif ((appConfig.getValueString(\"create_directory\") != \"\")) {\n\t\t\t\t\t// Handle the remote path creation and updating of the local database without performing a sync\n\t\t\t\t\tsyncEngineInstance.createDirectoryOnline(appConfig.getValueString(\"create_directory\"));\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// --remove-directory - Are we just deleting a directory online, without any sync being performed?\n\t\t\t\tif ((appConfig.getValueString(\"remove_directory\") != \"\")) {\n\t\t\t\t\t// Handle the remote path deletion without performing a sync\n\t\t\t\t\tsyncEngineInstance.deleteByPathNoSync(appConfig.getValueString(\"remove_directory\"));\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Are we renaming or moving a directory online?\n\t\t\t\t// \tonedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'\n\t\t\t\tif ((appConfig.getValueString(\"source_directory\") != \"\") && (appConfig.getValueString(\"destination_directory\") != \"\")) {\n\t\t\t\t\t// We are renaming or moving a directory\n\t\t\t\t\tsyncEngineInstance.moveOrRenameDirectoryOnline(appConfig.getValueString(\"source_directory\"), appConfig.getValueString(\"destination_directory\"));\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// --display-quota - Are we displaying the quota information?\n\t\t\t\tif (appConfig.getValueBool(\"display_quota\")) {\n\t\t\t\t\t// Query and respond with the quota details\n\t\t\t\t\tsyncEngineInstance.queryOneDriveForQuotaDetails();\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// --download-file - Are we downloading a single file from Microsoft OneDrive\n\t\t\t\tif ((appConfig.getValueString(\"download_single_file\") != \"\")) {\n\t\t\t\t\t// Handle downloading the single file\n\t\t\t\t\tsyncEngineInstance.downloadSingleFile(appConfig.getValueString(\"download_single_file\"));\n\t\t\t\t\t// Exit application\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If we get to this point, we have not performed a 'no-sync' task ..\n\t\t\t\t\n\t\t\t\t// Did we just authorise the client?\n\t\t\t\tif (appConfig.applicationAuthoriseResponseURIReceived) {\n\t\t\t\t\t// Authorisation activity\n\t\t\t\t\tif (exists(appConfig.refreshTokenFilePath)) {\n\t\t\t\t\t\t// OneDrive refresh token exists\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"The application has been successfully authorised, but no extra command options have been specified.\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(genericHelpMessage);\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\t\treturn EXIT_SUCCESS;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// We just authorised, but refresh_token does not exist .. probably an auth error?\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"Your application's authorisation was unsuccessful. Please review your URI response entry, then attempt authorisation again with a new URI response.\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\t\treturn EXIT_FAILURE;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No authorisation activity - print error message\n\t\t\t\t\tprintMissingOperationalSwitchesError();\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_FAILURE;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// API could not be initialised\n\t\t\taddLogEntry(\"The OneDrive API could not be initialised\");\n\t\t\treturn EXIT_FAILURE;\n\t\t}\n\t}\n\t\n\t// Configure the sync directory based on the runtimeSyncDirectory configured directory\n\tif (verboseLogging) {addLogEntry(\"All application operations will be performed in the configured local 'sync_dir' directory: \" ~ runtimeSyncDirectory, [\"verbose\"]);}\n\t// Try and set the 'sync_dir', attempt to create if it does not exist\n\ttry {\n\t\tif (!exists(runtimeSyncDirectory)) {\n\t\t\tif (debugLogging) {addLogEntry(\"runtimeSyncDirectory: Configured 'sync_dir' is missing locally. Creating: \" ~ runtimeSyncDirectory, [\"debug\"]);}\n\t\t\t\n\t\t\t// At this point 'sync_dir' is missing and we have requested to create it\n\t\t\t// However ... 'itemDB' is pointing to a valid database file\n\t\t\t// 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\n\t\t\t// In this scenario, the application, depending on the options being used, may attempt to delete all files online - which is not desirable\n\t\t\t// Do a sanity check here to ensure that there are no database entries\n\t\t\n\t\t\tif (itemDB.getTotalRowCount() == 1) {\n\t\t\t\t// Technically an 'empty database'\n\t\t\t\t// 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\n\t\t\t\ttry {\n\t\t\t\t\t// Attempt to create the sync dir we have been configured with\n\t\t\t\t\tmkdirRecurse(runtimeSyncDirectory);\n\t\t\t\t\t// Configure the applicable permissions for the folder\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting directory permissions for: \" ~ runtimeSyncDirectory, [\"debug\"]);}\n\t\t\t\t\truntimeSyncDirectory.setAttributes(appConfig.returnRequiredDirectoryPermissions());\n\t\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t\t// Creating the sync directory failed\n\t\t\t\t\taddLogEntry(\"ERROR: Unable to create the configured local 'sync_dir' directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_FAILURE;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Not an empty database\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"An application cache state issue has been detected where a --resync is required\", [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry();\n\t\t\t\treturn EXIT_RESYNC_REQUIRED;\n\t\t\t}\n\t\t}\n\t} catch (std.file.FileException e) {\n\t\t// Creating the sync directory failed\n\t\taddLogEntry(\"ERROR: Unable to test for the existence of the configured local 'sync_dir' directory: \" ~ e.msg);\n\t\t// Use exit scopes to shutdown API\n\t\treturn EXIT_FAILURE;\n\t}\n\t\n\t// Try and change to the working directory to the 'sync_dir' as configured\n\ttry {\n\t\tchdir(runtimeSyncDirectory);\n\t// A FileSystem exception was thrown when attempting to change to the configured 'sync_dir'\n\t} catch (FileException e) {\n\t\t// Log error message\n\t\taddLogEntry(\"FATAL: Unable to change to the configured local 'sync_dir' directory: \" ~ runtimeSyncDirectory);\n\t\t// A file system exception was generated\n\t\tdisplayFileSystemErrorMessage(e.msg, strip(getFunctionName!({})), runtimeSyncDirectory, FsErrorSeverity.fatal);\n\t\t// Use exit scopes to shutdown API as if we are unable to change to the 'sync_dir' we need to exit\n\t\treturn EXIT_FAILURE;\n\t}\n\t\n\t// Do we need to validate the runtimeSyncDirectory to check for the presence of a '.nosync' file\n\tcheckForNoMountScenario();\n\t\n\t// Is the sync engine initialised correctly?\n\tif (appConfig.syncEngineWasInitialised) {\n\t\t// Configure some initial variables\n\t\tstring singleDirectoryPath;\n\t\tstring localPath = \".\";\n\t\tstring remotePath = \"/\";\n\t\t\n\t\t// If not performing a --resync, check if there are interrupted downloads and/or uploads that need to be completed\n\t\tif (!appConfig.getValueBool(\"resync\")) {\n\t\t\t// Check if there are any downloads that need to be resumed\n\t\t\tif (syncEngineInstance.checkForResumableDownloads) {\n\t\t\t\t// Need to re-process the the 'resumable data' to resume the download\n\t\t\t\taddLogEntry(\"There are interrupted downloads that need to be resumed ...\");\n\t\t\t\t// Process the resumable download files\n\t\t\t\tsyncEngineInstance.processResumableDownloadFiles();\n\t\t\t}\n\t\t\t\n\t\t\t// Check if there are interrupted upload session(s)\n\t\t\tif (syncEngineInstance.checkForInterruptedSessionUploads) {\n\t\t\t\t// Need to re-process the session upload files to resume the failed session uploads\n\t\t\t\taddLogEntry(\"There are interrupted session uploads that need to be resumed ...\");\n\t\t\t\t// Process the session upload files\n\t\t\t\tsyncEngineInstance.processInterruptedSessionUploads();\n\t\t\t}\n\t\t} else {\n\t\t\t// Clean up any downloads that were due to be resumed, but will not be resumed due to --resync being used\n\t\t\tsyncEngineInstance.clearInterruptedDownloads();\n\t\t\t\n\t\t\t// Clean up any uploads that were due to be resumed, but will not be resumed due to --resync being used\n\t\t\tsyncEngineInstance.clearInterruptedSessionUploads();\n\t\t}\n\t\t\n\t\t// Are we doing a single directory operation (--single-directory) ?\n\t\tif (!appConfig.getValueString(\"single_directory\").empty) {\n\t\t\t// Ensure that the value stored for appConfig.getValueString(\"single_directory\") does not contain any extra quotation marks\n\t\t\tstring originalSingleDirectoryValue = appConfig.getValueString(\"single_directory\");\n\t\t\t// Strip quotation marks from provided path to ensure no issues within a Docker environment when using passed in values\n\t\t\tstring updatedSingleDirectoryValue = strip(originalSingleDirectoryValue, \"\\\"\");\n\t\t\t// Set singleDirectoryPath\n\t\t\tsingleDirectoryPath = updatedSingleDirectoryValue;\n\t\t\t\n\t\t\t// Ensure that this is a normalised relative path to runtimeSyncDirectory\n\t\t\tstring normalisedRelativePath = replace(buildNormalizedPath(absolutePath(singleDirectoryPath)), buildNormalizedPath(absolutePath(runtimeSyncDirectory)), \".\" );\n\t\t\t\n\t\t\t// The user provided a directory to sync within the configured 'sync_dir' path\n\t\t\t// This also validates if the path being used exists online and/or does not have a 'case-insensitive match'\n\t\t\tsyncEngineInstance.setSingleDirectoryScope(normalisedRelativePath);\n\t\t\t\n\t\t\t// Does the directory we want to sync actually exist locally?\n\t\t\tif (!exists(singleDirectoryPath)) {\n\t\t\t\t// The requested path to use with --single-directory does not exist locally within the configured 'sync_dir'\n\t\t\t\taddLogEntry(\"WARNING: The requested path for --single-directory does not exist locally. Creating requested path within \" ~ runtimeSyncDirectory, [\"info\", \"notify\"]);\n\t\t\t\t// Attempt path creation\n\t\t\t\ttry {\n\t\t\t\t\t// Attempt to create the required --single-directory path locally\n\t\t\t\t\tmkdirRecurse(singleDirectoryPath);\n\t\t\t\t\t// Configure the applicable permissions for the folder\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting directory permissions for: \" ~ singleDirectoryPath, [\"debug\"]);}\n\t\t\t\t\tsingleDirectoryPath.setAttributes(appConfig.returnRequiredDirectoryPermissions());\n\t\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t\t// Creating the sync directory failed\n\t\t\t\t\taddLogEntry(\"ERROR: Unable to create the required --single-directory path: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t\t// Use exit scopes to shutdown API\n\t\t\t\t\treturn EXIT_FAILURE;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Update the paths that we use to perform the sync actions\n\t\t\tlocalPath = singleDirectoryPath;\n\t\t\tremotePath = singleDirectoryPath;\n\t\t\t\n\t\t\t// Display that we are syncing from a specific path due to --single-directory\n\t\t\tif (verboseLogging) {addLogEntry(\"Syncing changes from this selected path: \" ~ singleDirectoryPath, [\"verbose\"]);}\n\t\t}\n\t\t\n\t\t// Handle SIGINT, SIGTERM and SIGSEGV signals\n\t\tsetupSignalHandler();\n\t\t\n\t\t// Are we doing a --sync operation? This includes doing any --single-directory operations\n\t\tif (appConfig.getValueBool(\"synchronize\")) {\n\t\t\t// We are not using this, so destroy it early\n\t\t\tobject.destroy(filesystemMonitor);\n\t\t\tfilesystemMonitor = null;\n\t\t\n\t\t\t// Did the user specify --upload-only?\n\t\t\tif (appConfig.getValueBool(\"upload_only\")) {\n\t\t\t\t// Perform the --upload-only sync process\n\t\t\t\tperformUploadOnlySyncProcess(localPath);\n\t\t\t}\n\t\t\t\n\t\t\t// Did the user specify --download-only?\n\t\t\tif (appConfig.getValueBool(\"download_only\")) {\n\t\t\t\t// Only download data from OneDrive\n\t\t\t\tsyncEngineInstance.syncOneDriveAccountToLocalDisk();\n\t\t\t\t// Perform the DB consistency check \n\t\t\t\t// This will also delete any out-of-sync flagged items if configured to do so\n\t\t\t\tsyncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();\n\t\t\t\t// Do we cleanup local files?\n\t\t\t\t// - Deletes of data from online will already have been performed, but what we are now doing is searching the local filesystem\n\t\t\t\t//   for any new data locally, that usually would be uploaded to OneDrive, but instead, because of the options being\n\t\t\t\t//   used, will need to be deleted from the local filesystem\n\t\t\t\tif (appConfig.getValueBool(\"cleanup_local_files\")) {\n\t\t\t\t\t// Perform the filesystem walk\n\t\t\t\t\tsyncEngineInstance.scanLocalFilesystemPathForNewData(localPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// If no use of --upload-only or --download-only\n\t\t\tif ((!appConfig.getValueBool(\"upload_only\")) && (!appConfig.getValueBool(\"download_only\"))) {\n\t\t\t\t// Perform the standard sync process\n\t\t\t\tperformStandardSyncProcess(localPath);\n\t\t\t}\n\t\t\n\t\t\t// Detail the outcome of the sync process\n\t\t\tdisplaySyncOutcome();\n\t\t}\n\t\t\n\t\t// Are we doing a --monitor operation?\n\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t// Update the flag given we are running with --monitor\n\t\t\tperformFileSystemMonitoring = true;\n\t\t\t\n\t\t\t// Set initial variable for when we last uploaded something or made an online change from a local inotify event\n\t\t\tlastLocalWrite = MonoTime.currTime() - dur!\"hours\"(24);\n\t\t\t\n\t\t\t// Is Display Manager Integration enabled?\n\t\t\tif (appConfig.getValueBool(\"display_manager_integration\")) {\n\t\t\t\t// Attempt to configure the desktop integration whilst the client is running in --monitor mode\n\t\t\t\tattemptFileManagerIntegration();\n\t\t\t}\n\t\t\n\t\t\t// If 'webhooks' are enabled, this is going to conflict with 'websockets' if the OS cURL library supports websockets\n\t\t\tif (appConfig.getValueBool(\"webhook_enabled\") && appConfig.curlSupportsWebSockets) {\n\t\t\t\t// We have to disable 'websocket' support\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"WARNING: WebSocket support has been disabled because Webhooks are already configured to monitor Microsoft Graph API changes.\");\n\t\t\t\taddLogEntry(\"         Only one API notification method can be active at a time.\");\n\t\t\t\taddLogEntry();\n\t\t\t\t// Set the flag that this will not be used\n\t\t\t\tappConfig.curlSupportsWebSockets = false;\n\t\t\t} else {\n\t\t\t\t// Double check scenario, this time 'false' checking 'webhook_enabled'\n\t\t\t\tif ((!appConfig.getValueBool(\"webhook_enabled\")) && (appConfig.curlSupportsWebSockets)) {\n\t\t\t\t\n\t\t\t\t\t// If we are doing --upload-only however .. we need to 'ignore' online change\n\t\t\t\t\tif (!appConfig.getValueBool(\"upload_only\")) {\n\t\t\t\t\t\t// Did the user configure to disable 'websocket' support?\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"disable_websocket_support\")) {\n\t\t\t\t\t\t\t// Log that we are attempting to enable WebSocket Support\n\t\t\t\t\t\t\taddLogEntry(\"Attempting to enable WebSocket support to monitor Microsoft Graph API changes in near real-time.\");\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Obtain the WebSocket Notification URL from the API endpoint\n\t\t\t\t\t\t\tsyncEngineInstance.obtainWebSocketNotificationURL();\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Were we able to correctly obtain the endpoint response and build the socket.io WS endpoint\n\t\t\t\t\t\t\tif (appConfig.websocketNotificationUrlAvailable) {\n\t\t\t\t\t\t\t\t// Notification URL is available\n\t\t\t\t\t\t\t\tif (oneDriveSocketIo is null) {\n\t\t\t\t\t\t\t\t\toneDriveSocketIo = new OneDriveSocketIo(thisTid, appConfig);\n\t\t\t\t\t\t\t\t\toneDriveSocketIo.start();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\taddLogEntry(\"Enabled WebSocket support to monitor Microsoft Graph API changes in near real-time.\");\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Unable to configure WebSocket support to monitor Microsoft Graph API changes in near real-time.\");\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting 'disable_websocket_support' to 'true' to force WebSockets to be disabled.\", [\"debug\"]);}\n\t\t\t\t\t\t\t\tappConfig.setValueBool(\"disable_websocket_support\" , true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// WebSocket Support has been disabled\n\t\t\t\t\t\t\taddLogEntry(\"WebSocket support has been disabled by user configuration.\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// --upload only being used\n\t\t\t\t\t\taddLogEntry(\"Online changes will not be monitored by WebSocket support due to --upload-only\");\n\t\t\t\t\t\t// Set the flag that this will not be used\n\t\t\t\t\t\tappConfig.curlSupportsWebSockets = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// What are the current values for the platform we are running on\n\t\t\tstring maxOpenFilesSoft = strip(to!string(getSoftOpenFilesLimit()));\n\t\t\tstring maxOpenFilesHard = strip(to!string(getHardOpenFilesLimit()));\n\t\t\t// What is the currently configured maximum inotify watches that can be used\n\t\t\tstring maxInotifyWatches = strip(getMaxInotifyWatches());\n\t\t\t\n\t\t\t// Start the monitor process\n\t\t\taddLogEntry(\"OneDrive synchronisation interval (seconds): \" ~ to!string(appConfig.getValueLong(\"monitor_interval\")));\n\t\t\t\n\t\t\t// If we are in a --download-only method of operation, the output of these is not required\n\t\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\t\tif (verboseLogging) {\n\t\t\t\t\taddLogEntry(\"Maximum allowed open files (soft):           \" ~ maxOpenFilesSoft, [\"verbose\"]);\n\t\t\t\t\taddLogEntry(\"Maximum allowed open files (hard):           \" ~ maxOpenFilesHard, [\"verbose\"]);\n\t\t\t\t\taddLogEntry(\"Maximum allowed inotify user watches:        \" ~ maxInotifyWatches, [\"verbose\"]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Configure the monitor class\n\t\t\tfilesystemMonitor = new Monitor(appConfig, selectiveSync);\n\t\t\t\n\t\t\t// Delegated function for when inotify detects a new local directory has been created\n\t\t\tfilesystemMonitor.onDirCreated = delegate(string path) {\n\t\t\t\t// Handle .folder creation if skip_dotfiles is enabled\n\t\t\t\tif ((appConfig.getValueBool(\"skip_dotfiles\")) && (isDotFile(path))) {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"[M] Skipping watching local path - .folder found & --skip-dot-files enabled: \" ~ path, [\"verbose\"]);}\n\t\t\t\t} else {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"[M] Local directory created: \" ~ path, [\"verbose\"]);}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsyncEngineInstance.scanLocalFilesystemPathForNewData(path);\n\t\t\t\t\t\tmarkLocalWrite();\n\t\t\t\t\t} catch (CurlException e) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Offline, cannot create remote dir: \" ~ path, [\"verbose\"]);}\n\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\taddLogEntry(\"Cannot create remote directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\t\n\t\t\t// Delegated function for when inotify detects a local file has been changed\n\t\t\tfilesystemMonitor.onFileChanged = delegate(string[] changedLocalFilesToUploadToOneDrive) {\n\t\t\t\t// Handle a potentially locally changed file\n\t\t\t\t// Logging for this event moved to handleLocalFileTrigger() due to threading and false triggers from scanLocalFilesystemPathForNewData() above\n\t\t\t\tsyncEngineInstance.handleLocalFileTrigger(changedLocalFilesToUploadToOneDrive);\n\t\t\t\tmarkLocalWrite();\n\t\t\t\tif (verboseLogging) {addLogEntry(\"[M] Total number of local file(s) added or changed: \" ~ to!string(changedLocalFilesToUploadToOneDrive.length), [\"verbose\"]);}\n\t\t\t};\n\n\t\t\t// Delegated function for when inotify detects a delete event\n\t\t\tfilesystemMonitor.onDelete = delegate(string path) {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"[M] Local item deleted: \" ~ path, [\"verbose\"]);}\n\t\t\t\ttry {\n\t\t\t\t\t// The path has been deleted .. we cannot use isDir or isFile to advise what was deleted. This is the best we can Do\n\t\t\t\t\taddLogEntry(\"The operating system sent a deletion notification. Trying to delete this item as requested: \" ~ path);\n\t\t\t\t\t// perform the delete action\n\t\t\t\t\tsyncEngineInstance.deleteByPath(path);\n\t\t\t\t\tmarkLocalWrite();\n\t\t\t\t} catch (CurlException e) {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Offline, cannot delete item: \" ~ path, [\"verbose\"]);}\n\t\t\t\t} catch (SyncException e) {\n\t\t\t\t\tif (e.msg == \"The item to delete is not in the local database\") {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Item cannot be deleted from Microsoft OneDrive because it was not found in the local database\", [\"verbose\"]);}\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"Cannot delete remote item: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t\t}\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// Path is gone locally, log and continue.\n\t\t\t\t\taddLogEntry(\"ERROR: The local file system returned an error with the following message: \" ~ e.msg, [\"verbose\"]);\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\taddLogEntry(\"Cannot delete remote item: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t}\n\t\t\t};\n\t\t\t\n\t\t\t// Delegated function for when inotify detects a move event\n\t\t\tfilesystemMonitor.onMove = delegate(string from, string to) {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"[M] Local item moved: \" ~ from ~ \" -> \" ~ to, [\"verbose\"]);}\n\t\t\t\ttry {\n\t\t\t\t\t// Handle .folder -> folder if skip_dotfiles is enabled\n\t\t\t\t\tif ((appConfig.getValueBool(\"skip_dotfiles\")) && (isDotFile(from))) {\n\t\t\t\t\t\t// .folder -> folder handling - has to be handled as a new folder\n\t\t\t\t\t\tsyncEngineInstance.scanLocalFilesystemPathForNewData(to);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsyncEngineInstance.uploadMoveItem(from, to);\n\t\t\t\t\t}\n\t\t\t\t\tmarkLocalWrite();\n\t\t\t\t} catch (CurlException e) {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Offline, cannot move item !\", [\"verbose\"]);}\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\taddLogEntry(\"Cannot move item: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t}\n\t\t\t};\n\t\t\t\n\t\t\t// Initialise the local filesystem monitor class using inotify to monitor for local filesystem changes\n\t\t\t// If we are in a --download-only method of operation, we do not enable local filesystem monitoring\n\t\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\t\t// Not using --download-only\n\t\t\t\ttry {\n\t\t\t\t\taddLogEntry(\"Initialising filesystem inotify monitoring ...\", [\"info\", \"notify\"]);\n\t\t\t\t\tfilesystemMonitor.initialise();\n\t\t\t\t\taddLogEntry(\"Performing initial synchronisation to ensure consistent local state ...\");\n\t\t\t\t} catch (MonitorException e) {\t\n\t\t\t\t\t// monitor class initialisation failed\n\t\t\t\t\taddLogEntry(\"ERROR: \" ~ e.msg);\n\t\t\t\t\treturn EXIT_FAILURE;\n\t\t\t\t}\n\t\t\t}\n\t\t\n\t\t\t// Filesystem monitor loop variables\n\t\t\t// Immutables\n\t\t\timmutable auto checkOnlineInterval = dur!\"seconds\"(appConfig.getValueLong(\"monitor_interval\"));\n\t\t\timmutable auto githubCheckInterval = dur!\"seconds\"(86400);\n\t\t\timmutable auto localEchoDebounce = dur!\"seconds\"(10);\n\t\t\timmutable ulong fullScanFrequency = appConfig.getValueLong(\"monitor_fullscan_frequency\");\n\t\t\timmutable ulong logOutputSuppressionInterval = appConfig.getValueLong(\"monitor_log_frequency\");\n\t\t\timmutable bool webhookEnabled = appConfig.getValueBool(\"webhook_enabled\");\n\t\t\timmutable string loopStartOutputMessage = \"################################################## NEW LOOP ##################################################\";\n\t\t\timmutable string loopStopOutputMessage = \"################################################ LOOP COMPLETE ###############################################\";\n\t\t\t\n\t\t\t// Changeable variables\n\t\t\tulong monitorLoopFullCount = 0;\n\t\t\tulong fullScanFrequencyLoopCount = 0;\n\t\t\tulong monitorLogOutputLoopCount = 0;\n\t\t\tMonoTime lastCheckTime = MonoTime.currTime();\n\t\t\tMonoTime lastGitHubCheckTime = MonoTime.currTime();\n\t\t\t\n\t\t\twhile (performFileSystemMonitoring) {\n\t\t\t\t// Do we need to validate the runtimeSyncDirectory to check for the presence of a '.nosync' file - the disk may have been ejected ..\n\t\t\t\tcheckForNoMountScenario();\n\t\t\t\n\t\t\t\t// If we are in a --download-only method of operation, there is no filesystem monitoring, so no inotify events to check\n\t\t\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\t\t\t// Process any inotify events\n\t\t\t\t\tprocessInotifyEvents(true);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// WebSocket and Webhook Notification Handling\n\t\t\t\tbool notificationReceived = false;\n\t\t\t\t\n\t\t\t\t// If we are doing --upload-only however .. we need to 'ignore' online change\n\t\t\t\tif (!appConfig.getValueBool(\"upload_only\")) {\n\t\t\t\t\t// Check for notifications pushed from Microsoft to the webhook\n\t\t\t\t\tif (webhookEnabled) {\n\t\t\t\t\t\t// Create a subscription on the first run, or renew the subscription\n\t\t\t\t\t\t// on subsequent runs when it is about to expire.\n\t\t\t\t\t\tif (oneDriveWebhook is null) {\n\t\t\t\t\t\t\toneDriveWebhook = new OneDriveWebhook(thisTid, appConfig);\n\t\t\t\t\t\t\toneDriveWebhook.serve();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\toneDriveWebhook.createOrRenewSubscription();\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// WebSocket support is enabled by default, but only if the version of libcurl supports it\n\t\t\t\t\t\tif (appConfig.curlSupportsWebSockets) {\n\t\t\t\t\t\t\t// Did the user configure to disable 'websocket' support?\n\t\t\t\t\t\t\tif (!appConfig.getValueBool(\"disable_websocket_support\")) {\n\t\t\t\t\t\t\t\t// Do we need to renew the notification URL?\n\t\t\t\t\t\t\t\tauto renewEarly = dur!\"seconds\"(120);\n\t\t\t\t\t\t\t\tif (appConfig.websocketNotificationUrlAvailable && appConfig.websocketUrlExpiry.length) {\n\t\t\t\t\t\t\t\t\tauto expiry = SysTime.fromISOExtString(appConfig.websocketUrlExpiry);\n\t\t\t\t\t\t\t\t\tauto now    = Clock.currTime(UTC());\n\t\t\t\t\t\t\t\t\tif (expiry - now <= renewEarly) {\n\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t// Obtain the WebSocket Notification URL from the API endpoint\n\t\t\t\t\t\t\t\t\t\t\tsyncEngineInstance.obtainWebSocketNotificationURL();\n\t\t\t\t\t\t\t\t\t\t\tif (debugLogging) addLogEntry(\"Refreshed WebSocket notification URL prior to expiry\", [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\t\t\t\t\t\tif (debugLogging) addLogEntry(\"Failed to refresh WebSocket notification URL: \" ~ e.msg, [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Get the current time this loop is starting\n\t\t\t\tauto currentTime = MonoTime.currTime();\n\t\t\t\t\n\t\t\t\t// Do we perform a sync with OneDrive?\n\t\t\t\tif ((currentTime - lastCheckTime >= checkOnlineInterval) || (monitorLoopFullCount == 0)) {\n\t\t\t\t\t// Increment relevant counters\n\t\t\t\t\tmonitorLoopFullCount++;\n\t\t\t\t\tfullScanFrequencyLoopCount++;\n\t\t\t\t\tmonitorLogOutputLoopCount++;\n\t\t\t\t\t\n\t\t\t\t\t// If full scan at a specific frequency enabled?\n\t\t\t\t\tif (fullScanFrequency > 0) {\n\t\t\t\t\t\t// Full Scan set for some 'frequency' - do we flag to perform a full scan of the online data?\n\t\t\t\t\t\tif (fullScanFrequencyLoopCount > fullScanFrequency) {\n\t\t\t\t\t\t\t// set full scan trigger for true up\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Enabling Full Scan True Up (fullScanFrequencyLoopCount > fullScanFrequency), resetting fullScanFrequencyLoopCount = 1\", [\"debug\"]);}\n\t\t\t\t\t\t\tfullScanFrequencyLoopCount = 1;\n\t\t\t\t\t\t\tappConfig.fullScanTrueUpRequired = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// unset full scan trigger for true up\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Disabling Full Scan True Up\", [\"debug\"]);}\n\t\t\t\t\t\t\tappConfig.fullScanTrueUpRequired = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No it is disabled - ensure this is false\n\t\t\t\t\t\tappConfig.fullScanTrueUpRequired = false;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Loop Start\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(loopStartOutputMessage, [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"Total Run-Time Loop Number:     \" ~ to!string(monitorLoopFullCount), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"Full Scan Frequency Loop Number: \" ~ to!string(fullScanFrequencyLoopCount), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\tSysTime startFunctionProcessingTime = Clock.currTime();\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Start Monitor Loop Time:        \" ~ to!string(startFunctionProcessingTime), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Do we perform any monitor console logging output suppression?\n\t\t\t\t\t// 'monitor_log_frequency' controls how often, in a non-verbose application output mode, how often \n\t\t\t\t\t// the full output of what is occurring is done. This is done to lessen the 'verbosity' of non-verbose \n\t\t\t\t\t// logging, but only when running in --monitor\n\t\t\t\t\tif (monitorLogOutputLoopCount > logOutputSuppressionInterval) {\n\t\t\t\t\t\t// re-enable the logging output as required\n\t\t\t\t\t\tmonitorLogOutputLoopCount = 1;\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Allowing initial sync log output\", [\"debug\"]);}\n\t\t\t\t\t\tappConfig.suppressLoggingOutput = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// do we suppress the logging output to absolute minimal\n\t\t\t\t\t\tif (monitorLoopFullCount == 1) {\n\t\t\t\t\t\t\t// application startup with --monitor\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Allowing initial sync log output\", [\"debug\"]);}\n\t\t\t\t\t\t\tappConfig.suppressLoggingOutput = false;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// only suppress if we are not doing --verbose or higher\n\t\t\t\t\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Suppressing --monitor log output\", [\"debug\"]);}\n\t\t\t\t\t\t\t\tappConfig.suppressLoggingOutput = true;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Allowing log output\", [\"debug\"]);}\n\t\t\t\t\t\t\t\tappConfig.suppressLoggingOutput = false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// How long has the application been running for?\n\t\t\t\t\tauto elapsedTime = Clock.currTime() - applicationStartTime;\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Application run-time thus far: \" ~ to!string(elapsedTime), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Need to re-validate that the client is still online for this loop\n\t\t\t\t\tif (testInternetReachability(appConfig)) {\n\t\t\t\t\t\t// Starting a sync - we are online\n\t\t\t\t\t\taddLogEntry(\"Starting a sync with Microsoft OneDrive\");\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Attempt to reset syncFailures from any prior loop\n\t\t\t\t\t\tsyncEngineInstance.resetSyncFailures();\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Update cached quota details from online as this may have changed online in the background outside of this application\n\t\t\t\t\t\tsyncEngineInstance.freshenCachedDriveQuotaDetails();\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Did the user specify --upload-only?\n\t\t\t\t\t\tif (appConfig.getValueBool(\"upload_only\")) {\n\t\t\t\t\t\t\t// Perform the --upload-only sync process\n\t\t\t\t\t\t\tperformUploadOnlySyncProcess(localPath, filesystemMonitor);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Perform the standard sync process\n\t\t\t\t\t\t\tperformStandardSyncProcess(localPath, filesystemMonitor);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Handle any new inotify events\n\t\t\t\t\t\tprocessInotifyEvents(true);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Detail the outcome of the sync process\n\t\t\t\t\t\tdisplaySyncOutcome();\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Cleanup sync process arrays\n\t\t\t\t\t\tsyncEngineInstance.cleanupArrays();\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Write WAL and SHM data to file for this loop and release memory used by in-memory processing\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Merge contents of WAL and SHM files into main database file\", [\"debug\"]);}\n\t\t\t\t\t\titemDB.performCheckpoint(\"PASSIVE\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Not online\n\t\t\t\t\t\taddLogEntry(\"Microsoft OneDrive service is not reachable at this time. Will re-try on next sync attempt.\");\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Output end of loop processing times\n\t\t\t\t\tSysTime endFunctionProcessingTime = Clock.currTime();\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"End Monitor Loop Time:                \" ~ to!string(endFunctionProcessingTime), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"Elapsed Monitor Loop Processing Time: \" ~ to!string((endFunctionProcessingTime - startFunctionProcessingTime)), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Release all the curl instances used during this loop\n\t\t\t\t\t// New curl instances will be established on next loop\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"CurlEngine Pool Size PRE Cleanup: \" ~ to!string(curlEnginePoolLength()), [\"debug\"]);}\n\t\t\t\t\treleaseAllCurlInstances(); // Release all CurlEngine instances\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"CurlEngine Pool Size POST Cleanup: \" ~ to!string(curlEnginePoolLength()) , [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Display memory details before garbage collection\n\t\t\t\t\tif (displayMemoryUsage) {\n\t\t\t\t\t\taddLogEntry(\"Monitor Loop Count:   \" ~ to!string(monitorLoopFullCount));\n\t\t\t\t\t\t// Get the current time in the local timezone\n\t\t\t\t\t\tauto timeStamp = leftJustify(Clock.currTime().toString(), 28, '0');\n\t\t\t\t\t\taddLogEntry(\"Timestamp:            \" ~ to!string(timeStamp));\n\t\t\t\t\t\taddLogEntry(\"Application Run Time: \" ~ to!string(elapsedTime));\n\t\t\t\t\t\t// Display memory stats before GC cleanup\n\t\t\t\t\t\tdisplayMemoryUsagePreGC();\n\t\t\t\t\t}\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\t\t// Return free memory to the OS\n\t\t\t\t\tGC.minimize();\n\t\t\t\t\t// Display memory details after garbage collection\n\t\t\t\t\tif (displayMemoryUsage) displayMemoryUsagePostGC();\n\t\t\t\t\t\n\t\t\t\t\t// Log that this loop is complete\n\t\t\t\t\tif (debugLogging) {addLogEntry(loopStopOutputMessage, [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// performSync complete, set lastCheckTime to current time\n\t\t\t\t\tlastCheckTime = MonoTime.currTime();\n\t\t\t\t\t\n\t\t\t\t\t// Developer break via config option\n\t\t\t\t\tif (appConfig.getValueLong(\"monitor_max_loop\") > 0) {\n\t\t\t\t\t\t// developer set option to limit --monitor loops\n\t\t\t\t\t\tif (monitorLoopFullCount == (appConfig.getValueLong(\"monitor_max_loop\"))) {\n\t\t\t\t\t\t\tperformFileSystemMonitoring = false;\n\t\t\t\t\t\t\taddLogEntry(\"Exiting after \" ~ to!string(monitorLoopFullCount) ~ \" loops due to developer set option\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (performFileSystemMonitoring) {\t\n\t\t\t\t\tauto nextCheckTime = lastCheckTime + checkOnlineInterval;\n\t\t\t\t\tcurrentTime = MonoTime.currTime();\n\t\t\t\t\tauto sleepTime = nextCheckTime - currentTime;\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Sleep for \" ~ to!string(sleepTime), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\tif (filesystemMonitor.initialised || webhookEnabled || oneDriveSocketIo !is null) {\n\n\t\t\t\t\t\tif (filesystemMonitor.initialised) {\n\t\t\t\t\t\t\t// If local monitor is on and is waiting (previous event was not from webhook)\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Obsidian Editor has been written in such a way that it is constantly writing each and every keystroke to a file.\n\t\t\t\t\t\t\t// Not only is this really bad application behaviour, for this client, this means the application is constantly writing to disk, thus attempting to upload file changes.\n\t\t\t\t\t\t\t// Unfortunately Obsidian on Linux does not provide a built-in way to disable atomic saves or switch to a backup-copy method via configuration.\n\t\t\t\t\t\t\tif (appConfig.getValueBool(\"delay_inotify_processing\")) {\n\t\t\t\t\t\t\t\tThread.sleep(dur!(\"seconds\")(to!int(appConfig.getValueLong(\"inotify_delay\"))));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Start the filesystem monitor (inotify) worker and wait for inotify event\n\t\t\t\t\t\t\tif (!notificationReceived) {\n\t\t\t\t\t\t\t\tfilesystemMonitor.send(true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Adjust sleepTime based on webhook/websocket only when NOT upload_only\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"upload_only\")) {\n\t\t\t\t\t\t\tif (webhookEnabled) {\n\t\t\t\t\t\t\t\tDuration nextWebhookCheckDuration = oneDriveWebhook.getNextExpirationCheckDuration();\n\t\t\t\t\t\t\t\tif (nextWebhookCheckDuration < sleepTime) sleepTime = nextWebhookCheckDuration;\n\t\t\t\t\t\t\t\tnotificationReceived = false;\n\t\t\t\t\t\t\t} else if (oneDriveSocketIo !is null && !appConfig.getValueBool(\"disable_websocket_support\") && appConfig.curlSupportsWebSockets) {\n\t\t\t\t\t\t\t\tDuration nextWebsocketCheckDuration = oneDriveSocketIo.getNextExpirationCheckDuration();\n\t\t\t\t\t\t\t\tif (nextWebsocketCheckDuration < sleepTime) sleepTime = nextWebsocketCheckDuration;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// ALWAYS wait for FS worker, but only track webhook/websocket if NOT '--upload-only'\n\t\t\t\t\t\tint res = 1;\n\t\t\t\t\t\tbool onlineSignal = false;\n\n\t\t\t\t\t\tif (appConfig.getValueBool(\"upload_only\")) {\n\t\t\t\t\t\t\treceiveTimeout(sleepTime, (int msg) { res = msg; });\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treceiveTimeout(sleepTime, (int msg) { res = msg; }, (ulong _) { onlineSignal = true; });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Debug logging of worker status\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"worker status = \" ~ to!string(res), [\"debug\"]);\n\t\t\t\t\t\t\tif (!appConfig.getValueBool(\"upload_only\")) {\n\t\t\t\t\t\t\t\taddLogEntry(\"notificationReceived = \" ~ to!string(onlineSignal), [\"debug\"]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Empirical evidence shows that Microsoft often sends multiple\n\t\t\t\t\t\t// notifications for one single change, so we need a loop to exhaust\n\t\t\t\t\t\t// all signals that were queued up by the webhook. The notifications\n\t\t\t\t\t\t// do not contain any actual changes, and we will always rely do the\n\t\t\t\t\t\t// delta endpoint to sync to latest. Therefore, only one sync run is\n\t\t\t\t\t\t// good enough to catch up for multiple notifications.\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Only process online notifications if NOT '--upload-only'\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"upload_only\") && onlineSignal) {\n\t\t\t\t\t\t\tint signalCount = 1;\n\t\t\t\t\t\t\twhile (true) {\n\t\t\t\t\t\t\t\tauto more = receiveTimeout(dur!\"seconds\"(-1), (ulong _) {});\n\t\t\t\t\t\t\t\tif (more) {\n\t\t\t\t\t\t\t\t\tsignalCount++;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tauto now = MonoTime.currTime();\n\t\t\t\t\t\t\t\t\tauto sinceLocal = now - lastLocalWrite;\n\t\t\t\t\t\t\t\t\tif (sinceLocal < localEchoDebounce) {\n\t\t\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\t\t\taddLogEntry(\n\t\t\t\t\t\t\t\t\t\t\t\t\"Debounced online refresh signal (\" ~\n\t\t\t\t\t\t\t\t\t\t\t\tto!string(sinceLocal.total!\"msecs\"()) ~ \" ms since local write; threshold \" ~\n\t\t\t\t\t\t\t\t\t\t\t\tto!string(localEchoDebounce.total!\"msecs\"()) ~ \" ms)\",\n\t\t\t\t\t\t\t\t\t\t\t\t[\"debug\"]\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t// Ignore this reflection; skip the immediate online scan.\n\t\t\t\t\t\t\t\t\t\t// Next push or the regular monitor cadence will pick up genuine remote changes.\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Get the signal timestamp - this is as close as possible to when this was received\n\t\t\t\t\t\t\t\t\tSysTime signalTimeStamp = Clock.currTime();\n\t\t\t\t\t\t\t\t\tsignalTimeStamp.fracSecs = Duration.zero;\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Log what signal we received\n\t\t\t\t\t\t\t\t\tif (webhookEnabled) {\n\t\t\t\t\t\t\t\t\t\tstring webhookLogEntry = format(\"Received %s signal(s) from Webhook handler (%s)\", to!string(signalCount), to!string(signalTimeStamp));\n\t\t\t\t\t\t\t\t\t\taddLogEntry(webhookLogEntry);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tstring websocketLogEntry = format(\"Received %s signal(s) from WebSocket handler (%s)\", to!string(signalCount), to!string(signalTimeStamp));\n\t\t\t\t\t\t\t\t\t\taddLogEntry(websocketLogEntry);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Perform online callback action\n\t\t\t\t\t\t\t\t\toneDriveOnlineCallback();\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Worker failure remains outside '--upload-only' filter\n\t\t\t\t\t\tif (res == -1) {\n\t\t\t\t\t\t\taddLogEntry(\"ERROR: Monitor worker failed.\");\n\t\t\t\t\t\t\tmonitorFailures = true;\n\t\t\t\t\t\t\tperformFileSystemMonitoring = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// no hooks available, nothing to check\n\t\t\t\t\t\tThread.sleep(sleepTime);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Exit application as the sync engine could not be initialised\n\t\taddLogEntry(\"Application Sync Engine could not be initialised correctly\");\n\t\t// Use exit scope\n\t\treturn EXIT_FAILURE;\n\t}\n\t\n\t// Exit application using exit scope\n\tif (!syncEngineInstance.syncFailures && !monitorFailures) {\n\t\treturn EXIT_SUCCESS;\n\t} else {\n\t\treturn EXIT_FAILURE;\n\t}\n}\n\n// Set default application threads\nvoid setDefaultApplicationThreads() {\n\t// Read in system values\n\tint configuredThreads = to!int(appConfig.getValueLong(\"threads\"));\n\tint systemCPUs = totalCPUs;\n\t\n\t// Warning if configuredThreads is too high\n\tif (configuredThreads > systemCPUs) {\n\t\taddLogEntry();\n\t\taddLogEntry(\"WARNING: Configured 'threads = \" ~ to!string(configuredThreads) ~ \"' exceeds available CPU cores (\" ~ to!string(systemCPUs) ~ \").\");\n\t\taddLogEntry(\"         This may lead to reduced performance, CPU contention, and instability. For best results, set 'threads' no higher than the number of physical CPU cores.\");\n\t\taddLogEntry();\n\t}\n\t\n\t// Set the default threads based on configured option\n\tdefaultPoolThreads(configuredThreads);\n}\n\n// Retrieves the maximum inotify watches allowed by the system\nstring getMaxInotifyWatches() {\n\t// Predefined Versions\n\t// https://dlang.org/spec/version.html#predefined-versions\n\tversion (linux) {\n\t\ttry {\n\t\t\t// Read max inotify watches from procfs on Linux\n\t\t\treturn strip(readText(\"/proc/sys/fs/inotify/max_user_watches\"));\n\t\t} catch (Exception e) {\n\t\t\treturn \"Unknown (Error reading /proc/sys/fs/inotify/max_user_watches)\";\n\t\t}\n\t} else version (FreeBSD) {\n\t\t// FreeBSD uses kqueue instead of inotify, no direct equivalent\n\t\treturn \"N/A (uses kqueue)\";\n\t} else version (OpenBSD) {\n\t\t// OpenBSD uses kqueue instead of inotify, no direct equivalent\n\t\treturn \"N/A (uses kqueue)\";\n\t} else {\n\t\treturn \"Unsupported platform\";\n\t}\n}\n\n// Print error message when --sync or --monitor has not been used and no valid 'no-sync' operation was requested\nvoid printMissingOperationalSwitchesError() {\n\t// notify the user that --sync or --monitor were missing\n\taddLogEntry();\n\taddLogEntry(\"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.\");\n\taddLogEntry();\n\taddLogEntry(\"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\");\n\taddLogEntry();\n}\n\n// Function used for WebSocket or Webhook callbacks to perform specific activities\nvoid oneDriveOnlineCallback() {\n\t// If we are in a --download-only method of operation, there is no filesystem monitoring, so no inotify events to check\n\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t// Handle inotify events\n\t\tprocessInotifyEvents(true);\n\t}\n\n\t// Sync any online change down to the local disk\n\t// If we are doing --upload-only however .. we need to 'ignore' online change\n\tif (!appConfig.getValueBool(\"upload_only\")) {\n\t\t// We are not doing an --upload-only scenario .. sync online change --> local\n\t\tsyncEngineInstance.syncOneDriveAccountToLocalDisk();\n\t}\n\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t// Handle inotify events\n\t\tprocessInotifyEvents(true);\n\t}\n}\n\n// Perform only an upload of data when using --upload-only\nvoid performUploadOnlySyncProcess(string localPath, Monitor filesystemMonitor = null) {\n\t// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive\n\tsyncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();\n\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t// Handle any inotify events whilst the DB was being scanned\n\t\tprocessInotifyEvents(true);\n\t}\n\t\n\t// Scan the configured 'sync_dir' for new data to upload\n\tsyncEngineInstance.scanLocalFilesystemPathForNewData(localPath);\n\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t// Handle any new inotify events whilst the local filesystem was being scanned\n\t\tprocessInotifyEvents(true);\n\t}\n}\n\n// Perform the normal application sync process\nvoid performStandardSyncProcess(string localPath, Monitor filesystemMonitor = null) {\n\t// If we are performing log suppression, output this message so the user knows what is happening\n\tif (appConfig.suppressLoggingOutput) {\n\t\taddLogEntry(\"Syncing changes from Microsoft OneDrive ...\");\n\t}\n\t\n\t// Zero out these arrays\n\tsyncEngineInstance.fileDownloadFailures = [];\n\tsyncEngineInstance.fileUploadFailures = [];\n\t\n\t// Which way do we sync first?\n\t// OneDrive first then local changes (normal operational process that uses OneDrive as the source of truth)\n\t// Local First then OneDrive changes (alternate operation process to use local files as source of truth)\n\tif (appConfig.getValueBool(\"local_first\")) {\n\t\t// Local data first \n\t\t// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive\n\t\tsyncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();\n\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t// Handle any inotify events whilst the DB was being scanned\n\t\t\tprocessInotifyEvents(true);\n\t\t}\n\t\t\n\t\t// Scan the configured 'sync_dir' for new data to upload to OneDrive\n\t\tsyncEngineInstance.scanLocalFilesystemPathForNewData(localPath);\n\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t// Handle any new inotify events whilst the local filesystem was being scanned\n\t\t\tprocessInotifyEvents(true);\n\t\t}\n\t\t\n\t\t// Download data from OneDrive last\n\t\tsyncEngineInstance.syncOneDriveAccountToLocalDisk();\n\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t// Cancel out any inotify events from downloading data\n\t\t\tprocessInotifyEvents(false);\n\t\t}\n\t\t\n\t\t// At this point, we have done a sync from:\n\t\t// local  -> online\n\t\t// online -> local\n\t\t//\n\t\t// Everything now should be 'in sync' and the database correctly populated with data\n\t\t// If --resync was used, we need to unset this as sync.d performs certain queries depending on if 'resync' is set or not\n\t\tif (appConfig.getValueBool(\"resync\")) {\n\t\t\t// unset 'resync' now that everything has been performed\n\t\t\tappConfig.setValueBool(\"resync\" , false);\n\t\t}\n\t} else {\n\t\t// Normal sync process\n\t\t// Download data from OneDrive first\n\t\tsyncEngineInstance.syncOneDriveAccountToLocalDisk();\n\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t// Cancel out any inotify events from downloading data\n\t\t\tprocessInotifyEvents(false);\n\t\t}\n\t\t\n\t\t// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive\n\t\tsyncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();\n\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t// Handle any inotify events whilst the DB was being scanned\n\t\t\tprocessInotifyEvents(true);\n\t\t}\n\t\t\t\n\t\t// Is --download-only NOT configured?\n\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\n\t\t\t// Scan the configured 'sync_dir' for new data to upload to OneDrive\n\t\t\tsyncEngineInstance.scanLocalFilesystemPathForNewData(localPath);\n\t\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t\t// Handle any new inotify events whilst the local filesystem was being scanned\n\t\t\t\tprocessInotifyEvents(true);\n\t\t\t}\n\t\t\t\n\t\t\t// If we are not doing a 'force_children_scan' perform a true-up\n\t\t\t// 'force_children_scan' is used when using /children rather than /delta and it is not efficient to re-run this exact same process twice\n\t\t\tif (!appConfig.getValueBool(\"force_children_scan\")) {\n\t\t\t\t// Perform the final true up scan to ensure we have correctly replicated the current online state locally\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\taddLogEntry(\"Performing a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process\");\n\t\t\t\t}\n\t\t\t\t// We pass in the 'appConfig.fullScanTrueUpRequired' value which then flags do we use the configured 'deltaLink'\n\t\t\t\t// If 'appConfig.fullScanTrueUpRequired' is true, we do not use the 'deltaLink' if we are in --monitor mode, thus forcing a full scan true up\n\t\t\t\tsyncEngineInstance.syncOneDriveAccountToLocalDisk();\n\t\t\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t\t\t// Cancel out any inotify events from downloading data\n\t\t\t\t\tprocessInotifyEvents(false);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// At this point, we have done a sync from:\n\t\t// online -> local\n\t\t// local  -> online (if not doing --download-only)\n\t\t// online -> local (if not doing --download-only)\n\t\t//\n\t\t// Everything now should be 'in sync' and the database correctly populated with data\n\t\t// If --resync was used, we need to unset this as sync.d performs certain queries depending on if 'resync' is set or not\n\t\tif (appConfig.getValueBool(\"resync\")) {\n\t\t\t// unset 'resync' now that everything has been performed\n\t\t\tappConfig.setValueBool(\"resync\" , false);\n\t\t}\n\t}\n}\n\n// Process any inotify events\nvoid processInotifyEvents(bool updateFlag) {\n\t// Attempt to process or cancel inotify events\n\t// filesystemMonitor.update will throw this, thus needs to be caught\n\t//   monitor.MonitorException@src/monitor.d(549): inotify queue overflow: some events may be lost (Interrupted system call)\n\ttry {\n\t\t// Process any inotify events or cancel events based on flag value\n\t\t// True = process\n\t\t// False = cancel\n\t\tfilesystemMonitor.update(updateFlag);\n\t} catch (MonitorException e) {\n\t\t// Catch any exceptions thrown by inotify / monitor engine\n\t\taddLogEntry(\"ERROR: The following inotify error was generated: \" ~ e.msg);\n\t}\n}\n\n// Display the sync outcome\nvoid displaySyncOutcome() {\n\t// Detail any download or upload transfer failures\n\tsyncEngineInstance.displaySyncFailures();\n\t\n\t// Sync is either complete or partially complete\n\tif (!syncEngineInstance.syncFailures) {\n\t\t// No download or upload issues\n\t\tif (!appConfig.getValueBool(\"monitor\")) addLogEntry(); // Add an additional line break so that this is clear when using --sync\n\t\taddLogEntry(\"Sync with Microsoft OneDrive is complete\");\n\t} else {\n\t\taddLogEntry();\n\t\taddLogEntry(\"Sync with Microsoft OneDrive has completed, however there are items that failed to sync.\");\n\t\t// 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.\n\t\t// 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\n\t\tif (!syncEngineInstance.fileDownloadFailures.empty) {\n\t\t\taddLogEntry(\"To fix any download failures you may need to perform a --resync to ensure this system is correctly synced with your Microsoft OneDrive Account\");\n\t\t}\n\t\tif (!syncEngineInstance.fileUploadFailures.empty) {\n\t\t\taddLogEntry(\"To fix any upload failures you may need to perform a --resync to ensure this system is correctly synced with your Microsoft OneDrive Account\");\n\t\t}\n\t\t// So that from a logging perspective these messages are clear, add a line break in\n\t\taddLogEntry();\n\t}\n}\n\n// Perform database file removal\nvoid processResyncDatabaseRemoval(string databaseFilePathToRemove) {\n\t// Log what we are doing\n\tif (debugLogging) {addLogEntry(\"Testing if we have exclusive access to local database file\", [\"debug\"]);}\n\t\n\t// Are we the only running instance? Test that we can open the database file path\n\titemDB = new ItemDatabase(databaseFilePathToRemove);\n\t\n\t// did we successfully initialise the database class?\n\tif (!itemDB.isDatabaseInitialised()) {\n\t\t// no .. destroy class\n\t\titemDB = null;\n\t\t// exit application - void function, force exit this way\n\t\texit(EXIT_FAILURE);\n\t}\n\t\n\t// If we have exclusive access we will not have exited\n\t// destroy access test\n\titemDB = null;\n\t// delete application sync state\n\taddLogEntry(\"Deleting the saved application sync status ...\");\n\tif (!dryRun) {\n\t\tsafeRemove(databaseFilePathToRemove);\n\t} else {\n\t\t// --dry-run scenario ... technically we should not be making any local file changes .......\n\t\taddLogEntry(\"DRY-RUN: Not removing the saved application sync status\");\n\t}\n}\n\n// Clean up the local database files\nvoid cleanupDatabaseFiles(string activeDatabaseFileName) {\n\t// Temp variables\n\tstring databaseShmFile = activeDatabaseFileName ~ \"-shm\";\n\tstring databaseWalFile = activeDatabaseFileName ~ \"-wal\";\n\t\n\t// Are we performing a --dry-run?\n\tif (dryRun) {\n\t\t// If the dry run database exists, clean this up\n\t\tif (exists(activeDatabaseFileName)) {\n\t\t\t// remove the dry run database file\n\t\t\tif (debugLogging) {addLogEntry(\"DRY-RUN: Removing items-dryrun.sqlite3 as it still exists for some reason\", [\"debug\"]);}\n\t\t\tsafeRemove(activeDatabaseFileName);\n\t\t}\n\t} else {\n\t\t// 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\n\t\tif (exists(appConfig.databaseFilePathDryRun)) {\n\t\t\tif (debugLogging) {addLogEntry(\"Removing items-dryrun.sqlite3 as it still exists for some reason post being used for non-dryrun operations\", [\"debug\"]);}\n\t\t\tsafeRemove(appConfig.databaseFilePathDryRun);\n\t\t}\n\t}\n\t\n\t// Silent cleanup of -shm file if it exists\n\tif (exists(databaseShmFile)) {\n\t\t// Configure the log message\n\t\tstring logMessage = \"Removing \" ~ baseName(databaseShmFile) ~ \" as it still exists for some reason\";\n\t\t// Is this a --dry-run scenario\n\t\tif (dryRun) {\n\t\t\tlogMessage = \"DRY-RUN: \" ~ logMessage;\n\t\t}\n\t\n\t\t// Remove -shm file\n\t\tif (debugLogging) {addLogEntry(logMessage, [\"debug\"]);}\n\t\tsafeRemove(databaseShmFile);\n\t}\n\t\n\t// Silent cleanup of wal files if it exists\n\tif (exists(databaseWalFile)) {\n\t\t// Configure the log message\n\t\tstring logMessage = \"Removing \" ~ baseName(databaseWalFile) ~ \" as it still exists for some reason\";\n\t\t// Is this a --dry-run scenario\n\t\tif (dryRun) {\n\t\t\tlogMessage = \"DRY-RUN: \" ~ logMessage;\n\t\t}\n\t\t\n\t\t// Remove -wal file\n\t\tif (debugLogging) {addLogEntry(logMessage, [\"debug\"]);}\n\t\tsafeRemove(databaseWalFile);\n\t}\n}\n\n// Perform a check to see if this is a mount point, and if the 'mount' has gone\nvoid checkForNoMountScenario() {\n\t// 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\n\tif (appConfig.getValueBool(\"check_nomount\")) {\n\t\t// we were asked to check the mount point for the presence of a '.nosync' file\n\t\tif (exists(\".nosync\")) {\n\t\t\taddLogEntry(\"ERROR: .nosync file found in directory mount point. Aborting application startup process to safeguard data.\", [\"info\", \"notify\"]);\n\t\t\t// Perform the shutdown process\n\t\t\tperformSynchronisedExitProcess(\"check_nomount\");\n\t\t\t// Exit\n\t\t\texit(EXIT_FAILURE);\n\t\t}\n\t}\n}\n\n// Setup a signal handler for catching SIGINT, SIGTERM and SIGSEGV (CTRL-C and others) during application execution\nvoid setupSignalHandler() {\n\tsigaction_t action;\n\taction.sa_handler = &exitViaSignalHandler; // Direct function pointer assignment\n\tsigemptyset(&action.sa_mask); // Initialize the signal set to empty\n\taction.sa_flags = 0;\n\tsigaction(SIGINT, &action, null);  // Interrupt from keyboard\n\tsigaction(SIGTERM, &action, null); // Termination signal\n\tsigaction(SIGSEGV, &action, null); // Invalid Memory Access signal\n}\n\n// Catch SIGINT (CTRL-C), SIGTERM (kill) and SIGSEGV (invalid memory access), handle rapid repeat CTRL-C presses\nextern(C) nothrow @nogc @system void exitViaSignalHandler(int signo) {\n\n\t// Update global exitHandlerTriggered flag so that objects that depend on this know we are shutting down\n\texitHandlerTriggered = true;\n\t\n\t// Catch the generation of SIGSEV post SIGINT or SIGTERM event\n    if (signo == SIGSEGV) {\n\t\t// Was SIGTERM used?\n\t\tif (!sigtermHandlerTriggered) {\n\t\t\t// No .. so most likely SIGINT (CTRL-C) - lets check\n\t\t\tif (signo == SIGINT) {\n\t\t\t\t// Yes - SIGINT was used\n\t\t\t\tprintf(\"Due to a termination signal, internal processing stopped abruptly. The application will now exit in a unclean manner.\\n\");\n\t\t\t\texit(130);\n\t\t\t} else {\n\t\t\t\t// Confirmed as SIGSEGV, but not SIGINT and SIGTERM not used\n\t\t\t\tprintf(\"FATAL: Segmentation fault (SIGSEGV). The application encountered an internal error and will now exit in a unclean manner.\\n\");\n\t\t\t\texit(139);\n\t\t\t}\n\t\t} else {\n\t\t\t// High probability of being shutdown by systemd, for example: systemctl --user stop onedrive\n\t\t\t// Exit in a manner that does not trigger an exit failure in systemd\n\t\t\texit(0);\n\t\t}\n\t}\n\t\n\tif (signo == SIGTERM) {\n\t\t// systemd will use SIGTERM to terminate a running process\n\t\tsigtermHandlerTriggered = true;\n\t}\n\n\tif (shutdownInProgress) {\n\t\treturn;  // Ignore subsequent presses\n\t} else {\n\t\t// Disable logging suppression\n\t\tappConfig.suppressLoggingOutput = false;\n\t\t// Flag we are shutting down\n\t\tshutdownInProgress = true;\n\t\t\n\t\ttry {\n\t\t\tassumeNoGC ( () {\n\t\t\t\t// Log that a termination signal was caught\n\t\t\t\taddLogEntry(\"\\nReceived termination signal, attempting to cleanly shutdown application\");\n\t\t\t\t// Try and shutdown in a safe and synchronised manner\n\t\t\t\tperformSynchronisedExitProcess(\"SIGINT-SIGTERM-HANDLER\");\n\t\t\t})();\n\t\t} catch (Exception e) {\n\t\t\t// Any output here will cause a GC allocation\n\t\t\t// - Error: `@nogc` function `main.exitHandler` cannot call non-@nogc function `std.stdio.writeln!string.writeln`\n\t\t\t// - Error: cannot use operator `~` in `@nogc` function `main.exitHandler`\n\t\t\t// writeln(\"Exception during shutdown: \" ~ e.msg);\n\t\t}\n\t\t// Exit the process with the provided exit code\n\t\texit(signo);\n\t}\n}\n\n// Handle application exit\nvoid performSynchronisedExitProcess(string scopeCaller = null) {\n\tsynchronized {\n\t\t// Perform cleanup and shutdown of various services and resources\n\t\ttry {\n\t\t\t// Log who called this function\n\t\t\tif (debugLogging) {addLogEntry(\"performSynchronisedExitProcess called by: \" ~ scopeCaller, [\"debug\"]);}\n\t\t\t// Remove Desktop integration\n\t\t\tif(performFileSystemMonitoring) {\n\t\t\t\t// Was desktop integration enabled?\n\t\t\t\tif (appConfig.getValueBool(\"display_manager_integration\")) {\n\t\t\t\t\t// Attempt removal\n\t\t\t\t\tattemptFileManagerIntegrationRemoval();\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Shutdown the OneDrive Webhook instance\n\t\t\tshutdownOneDriveWebhook();\n\t\t\t// Shutdown the OneDrive WebSocket instance\n\t\t\tshutdownOneDriveSocketIo();\n\t\t\t// Shutdown any local filesystem monitoring\n\t\t\tshutdownFilesystemMonitor();\n\t\t\t// Shutdown the sync engine\n\t\t\tif (scopeCaller == \"SIGINT-SIGTERM-HANDLER\") {\n\t\t\t\t// Wait for all parallel jobs that depend on the database being available to complete\n\t\t\t\taddLogEntry(\"Waiting for any existing upload|download process to complete\");\n\t\t\t}\n\t\t\tshutdownSyncEngine();\n\t\t\t// Release all CurlEngine instances\n\t\t\treleaseAllCurlInstances();\n\t\t\t// Shutdown the client side filtering objects\n\t\t\tshutdownSelectiveSync();\n\t\t\t// Shutdown the database\n\t\t\tshutdownDatabase();\n\t\t\t// Shutdown the application configuration objects - nothing should be active now\n\t\t\tshutdownAppConfig();\n\t\t\t// Shutdown application logging\n\t\t\tshutdownApplicationLogging();\n\t\t} catch (Exception e) {\n            addLogEntry(\"Error during performStandardExitProcess: \" ~ e.toString(), [\"error\"]);\n        }\n\t}\n}\n\nvoid shutdownOneDriveWebhook() {\n    if (oneDriveWebhook !is null) {\n\t\tif (debugLogging) {addLogEntry(\"Shutting down OneDrive Webhook instance\", [\"debug\"]);}\n\t\toneDriveWebhook.stop();\n        object.destroy(oneDriveWebhook);\n        oneDriveWebhook = null;\n\t\tif (debugLogging) {addLogEntry(\"Shutdown of OneDrive Webhook instance is complete\", [\"debug\"]);}\n    }\n}\n\nvoid shutdownOneDriveSocketIo() {\n    if (oneDriveSocketIo !is null) {\n        if (debugLogging) addLogEntry(\"Shutting down OneDrive WebSocket instance\", [\"debug\"]);\n        oneDriveSocketIo.stop();\n        object.destroy(oneDriveSocketIo);\n        oneDriveSocketIo = null;\n        if (debugLogging) addLogEntry(\"Shutdown of OneDrive WebSocket instance complete\", [\"debug\"]);\n    }\n}\n\nvoid shutdownFilesystemMonitor() {\n    if (filesystemMonitor !is null) {\n\t\tif (debugLogging) {addLogEntry(\"Shutting down Filesystem Monitoring instance\", [\"debug\"]);}\n\t\tfilesystemMonitor.shutdown();\n        object.destroy(filesystemMonitor);\n        filesystemMonitor = null;\n\t\tif (debugLogging) {addLogEntry(\"Shutdown of Filesystem Monitoring instance is complete\", [\"debug\"]);}\n    }\n}\n\nvoid shutdownSelectiveSync() {\n    if (selectiveSync !is null) {\n\t\tif (debugLogging) {addLogEntry(\"Shutting down Client Side Filtering instance\", [\"debug\"]);}\n\t\tselectiveSync.shutdown();\n        object.destroy(selectiveSync);\n        selectiveSync = null;\n\t\tif (debugLogging) {addLogEntry(\"Shutdown of Client Side Filtering instance is complete\", [\"debug\"]);}\n    }\n}\n\nvoid shutdownSyncEngine() {\n    if (syncEngineInstance !is null) {\n\t\tif (debugLogging) {addLogEntry(\"Shutting down Sync Engine instance\", [\"debug\"]);}\n\t\tsyncEngineInstance.shutdown(); // Make sure any running thread completes first\n        object.destroy(syncEngineInstance);\n        syncEngineInstance = null;\n\t\tif (debugLogging) {addLogEntry(\"Shutdown Sync Engine instance is complete\", [\"debug\"]);}\n    }\n}\n\nvoid shutdownDatabase() {\n    if (itemDB !is null && itemDB.isDatabaseInitialised()) {\n\t\tif (debugLogging) {addLogEntry(\"Shutting down Database instance\", [\"debug\"]);}\n\t\t\n\t\t// Write WAL and SHM data to file\n\t\tif (debugLogging) {addLogEntry(\"Merge contents of WAL and SHM files into main database file before shutting down database\", [\"debug\"]);}\n\t\titemDB.performCheckpoint(\"TRUNCATE\");\n\t\t\n\t\t// Do we perform a database vacuum?\n\t\tif (performDatabaseVacuum) {\n\t\t\t// Logging to attempt this is denoted from performVacuum() - so no need to confirm here\n\t\t\titemDB.performVacuum();\n\t\t\t// If this completes, it is denoted from performVacuum() - so no need to confirm here\n\t\t}\n\t\t\n\t\t // Close the DB File Handle\n\t\titemDB.closeDatabaseFile();\n\t\tobject.destroy(itemDB);\n\t\tcleanupDatabaseFiles(runtimeDatabaseFile);\n\t\titemDB = null;\n\t\tif (debugLogging) {addLogEntry(\"Shutdown of Database instance is complete\", [\"debug\"]);}\n    }\n}\n\nvoid shutdownAppConfig() {\n    if (appConfig !is null) {\n\t\tif (debugLogging) {addLogEntry(\"Shutting down Application Configuration instance\", [\"debug\"]);}\n\t\tobject.destroy(appConfig);\n        appConfig = null;\n\t\tif (debugLogging) {addLogEntry(\"Shutdown of Application Configuration instance is complete\", [\"debug\"]);}\n    }\n}\n\nvoid shutdownApplicationLogging() {\n\t// Log that we are exiting\n\tif (loggingStillInitialised()) {\n\t\tif (loggingActive()) {\n\t\t\t// join all threads\n\t\t\tthread_joinAll();\n\t\t\tif (debugLogging) {addLogEntry(\"Application is exiting\", [\"debug\"]);}\n\t\t\taddLogEntry(\"#######################################################################################################################################\", [\"logFileOnly\"]);\n\t\t\t// Destroy the shared logging buffer which flushes any remaining logs\n\t\t\tif (debugLogging) {addLogEntry(\"Shutting down Application Logging instance\", [\"debug\"]);}\n\t\t\t// Allow any logging complete before we exit\n\t\t\tThread.sleep(dur!(\"msecs\")(500));\n\t\t\t// Shutdown Logging which also sets logBuffer to null\n\t\t\tshutdownLogging();\n\t\t}\n\t}\n}\n\nstring compilerDetails() {\n\tversion(DigitalMars) enum compiler = \"DMD\";\n\telse version(LDC)    enum compiler = \"LDC\";\n\telse version(GNU)    enum compiler = \"GDC\";\n\telse enum compiler = \"Unknown compiler\";\n\tstring compilerString = compiler ~ \" \" ~ to!string(__VERSION__);\n\treturn compilerString;\n}\n\nvoid attemptFileManagerIntegration() {\n\t// Are we running under a Desktop Manager (GNOME or KDE)?\n\tif (appConfig.isGuiSessionDetected()) {\n\t\t// Generate desktop hints\n\t\tauto hints = appConfig.detectDesktop();\n\t\t\n\t\t// GNOME Desktop File Manager integration\n\t\tif (hints.gnome) {\n\t\t\t// Attempt integration\n\t\t\tappConfig.addGnomeBookmark();\n\t\t\tappConfig.setOneDriveFolderIcon();\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// KDE Desktop File Manager integration\n\t\tif (hints.kde) {\n\t\t\t// Attempt integration\n\t\t\tappConfig.addKDEPlacesEntry();\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid attemptFileManagerIntegrationRemoval() {\n\t// Are we running under a Desktop Manager (GNOME or KDE)?\n\tif (appConfig.isGuiSessionDetected()) {\n\t\t// Generate desktop hints\n\t\tauto hints = appConfig.detectDesktop();\n\t\t\n\t\t// GNOME Desktop File Manager integration removal\n\t\tif (hints.gnome) {\n\t\t\t// Attempt integration removal\n\t\t\tappConfig.removeGnomeBookmark();\n\t\t\tappConfig.removeOneDriveFolderIcon();\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// KDE Desktop File Manager integration removal\n\t\tif (hints.kde) {\n\t\t\t// Attempt integration removal\n\t\t\tappConfig.removeKDEPlacesEntry();\n\t\t\treturn;\n\t\t}\t\n\t}\n}\n"
  },
  {
    "path": "src/monitor.d",
    "content": "// What is this module called?\nmodule monitor;\n\n// What does this module require to function?\nimport core.stdc.errno;\nimport core.stdc.stdlib;\nimport core.sys.linux.sys.inotify;\nimport core.sys.posix.poll;\nimport core.sys.posix.unistd;\nimport core.sys.posix.sys.select;\nimport core.thread;\nimport core.time;\nimport std.algorithm;\nimport std.concurrency;\nimport std.exception;\nimport std.file;\nimport std.path;\nimport std.process;\nimport std.regex;\nimport std.stdio;\nimport std.string;\nimport std.conv;\nimport core.sync.mutex;\n\n// What other modules that we have created do we need to import?\nimport config;\nimport util;\nimport log;\nimport clientSideFiltering;\n\n// Relevant inotify events\nversion(FreeBSD) {\n     private immutable uint32_t mask = IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_MOVE;\n} else {\n     private immutable uint32_t mask = IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_MOVE | IN_IGNORED | IN_Q_OVERFLOW;\n}\n\nclass MonitorException: ErrnoException {\n    @safe this(string msg, string file = __FILE__, size_t line = __LINE__) {\n        super(msg, file, line);\n    }\n}\n\nclass MonitorBackgroundWorker {\n\t// inotify file descriptor\n\tint fd;\n\tPipe p;\n\tbool isAlive;\n\n\tthis() {\n\t\tisAlive = true;\n\t\tp = pipe();\n\t}\n\n\tshared void initialise() {\n\t\tfd = inotify_init();\n\t\tif (fd < 0) throw new MonitorException(\"inotify_init failed\");\n\t}\n\n\t// Add this path to be monitored\n\tshared int addInotifyWatch(string pathname) {\n\t\tint wd = inotify_add_watch(fd, toStringz(pathname), mask);\n\t\tif (wd < 0) {\n\t\t\tif (errno() == ENOSPC) {\n\t\t\t\t// Predefined Versions\n\t\t\t\t// https://dlang.org/spec/version.html#predefined-versions\n\t\t\t\tversion (linux) {\n\t\t\t\t\t// Read max inotify watches from procfs on Linux\n\t\t\t\t\tulong maxInotifyWatches = to!int(strip(readText(\"/proc/sys/fs/inotify/max_user_watches\")));\n\t\t\t\t\taddLogEntry(\"The user limit on the total number of inotify watches has been reached.\");\n\t\t\t\t\taddLogEntry(\"Your current limit of inotify watches is: \" ~ to!string(maxInotifyWatches));\n\t\t\t\t\taddLogEntry(\"It is recommended that you change the max number of inotify watches to at least double your existing value.\");\n\t\t\t\t\taddLogEntry(\"To change the current max number of watches to \" ~ to!string((maxInotifyWatches * 2)) ~ \" run:\");\n\t\t\t\t\taddLogEntry(\"EXAMPLE: sudo sysctl fs.inotify.max_user_watches=\" ~ to!string((maxInotifyWatches * 2)));\n\t\t\t\t} else {\n\t\t\t\t\t// some other platform\n\t\t\t\t\taddLogEntry(\"The user limit on the total number of inotify watches has been reached.\");\n\t\t\t\t\taddLogEntry(\"Please seek support from your distribution on how to increase the max number of inotify watches to at least double your existing value.\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (errno() == 13) {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"WARNING: inotify_add_watch failed - permission denied: \" ~ pathname, [\"verbose\"]);}\n\t\t\t}\n\t\t\t// Flag any other errors\n\t\t\taddLogEntry(\"ERROR: inotify_add_watch failed: \" ~ pathname);\n\t\t\treturn wd;\n\t\t}\n\t\t\n\t\t// Add path to inotify watch - required regardless if a '.folder' or 'folder'\n\t\tif (debugLogging) {addLogEntry(\"inotify_add_watch successfully added for: \" ~ pathname, [\"debug\"]);}\n\t\t\n\t\t// Do we log that we are monitoring this directory?\n\t\tif (isDir(pathname)) {\n\t\t\t// Log that this is directory is being monitored\n\t\t\tif (verboseLogging) {addLogEntry(\"Monitoring directory: \" ~ pathname, [\"verbose\"]);}\n\t\t}\n\t\treturn wd;\n\t}\n\n\tshared int removeInotifyWatch(int wd) {\n\t\tassert(fd > 0, \"File descriptor 'fd' is invalid.\");\n\t\tassert(wd > 0, \"Watch descriptor 'wd' is invalid.\");\n\t\t// Debug logging of the inotify watch being removed\n\t\tif (debugLogging) {addLogEntry(\"Attempting to remove inotify watch: fd=\" ~ fd.to!string ~ \", wd=\" ~ wd.to!string, [\"debug\"]);}\n\t\t// return the value of performing the action\n\t\treturn inotify_rm_watch(fd, wd);\n\t}\n\n\tshared void watch(Tid callerTid) {\n\t\t// On failure, send -1 to caller\n\t\tint res;\n\n\t\t// wait for the caller to be ready\n\t\treceiveOnly!int();\n\n\t\twhile (isAlive) {\n\t\t\tfd_set fds;\n\t\t\tFD_ZERO (&fds);\n\t\t\tFD_SET(fd, &fds);\n\t\t\t// Listen for messages from the caller\n\t\t\tFD_SET((cast()p).readEnd.fileno, &fds);\n\t\t\t\n\t\t\tres = select(FD_SETSIZE, &fds, null, null, null);\n\n\t\t\tif(res == -1) {\n\t\t\t\tif(errno() == EINTR) {\n\t\t\t\t\t// Received an interrupt signal but no events are available\n\t\t\t\t\t// directly watch again\n\t\t\t\t} else {\n\t\t\t\t\t// Error occurred, tell caller to terminate.\n\t\t\t\t\tcallerTid.send(-1);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Wake up caller\n\t\t\t\tcallerTid.send(1);\n\n\t\t\t\t// wait for the caller to be ready\n\t\t\t\tif (isAlive)\n\t\t\t\t\tisAlive = receiveOnly!bool();\n\t\t\t}\n\t\t}\n\t}\n\n\tshared void interrupt() {\n\t\tisAlive = false;\n\t\t(cast()p).writeEnd.writeln(\"done\");\n\t\t(cast()p).writeEnd.flush();\n\t}\n\n\tshared void shutdown() {\n\t\tisAlive = false;\n\t\tif (fd > 0) {\n\t\t\tclose(fd);\n\t\t\tfd = 0;\n\t\t\t(cast()p).close();\n\t\t}\n\t}\n}\n\nvoid startMonitorJob(shared(MonitorBackgroundWorker) worker, Tid callerTid) {\n\ttry {\n    \tworker.watch(callerTid);\n\t} catch (OwnerTerminated error) {\n\t\t// caller is terminated\n\t\tworker.shutdown();\n\t}\n}\n\nenum ActionType {\n\tmoved,\n\tdeleted, \n\tchanged,\n\tcreateDir\n}\n\nstruct Action {\n\tActionType type;\n\tbool skipped;\n\tstring src;\n\tstring dst;\n}\n\nstruct ActionHolder {\n\tAction[] actions;\n\tsize_t[string] srcMap;\n\n\tvoid append(ActionType type, string src, string dst=null) {\n\t\tsize_t[] pendingTargets;\n\t\tswitch (type) {\n\t\t\tcase ActionType.changed:\n\t\t\t\tif (src in srcMap && actions[srcMap[src]].type == ActionType.changed) {\n\t\t\t\t\t// skip duplicate operations\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase ActionType.createDir:\n\t\t\t\tbreak;\n\t\t\tcase ActionType.deleted:\n\t\t\t\tif (src in srcMap) {\n\t\t\t\t\tsize_t pendingTarget = srcMap[src];\n\t\t\t\t\t// Skip operations require reading local file that is gone\n\t\t\t\t\tswitch (actions[pendingTarget].type) {\n\t\t\t\t\t\tcase ActionType.changed:\n\t\t\t\t\t\tcase ActionType.createDir:\n\t\t\t\t\t\t\tactions[srcMap[src]].skipped = true;\n\t\t\t\t\t\t\tsrcMap.remove(src);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase ActionType.moved:\n\t\t\t\tfor(int i = 0; i < actions.length; i++) {\n\t\t\t\t\t// Only match for latest operation\n\t\t\t\t\tif (actions[i].src in srcMap) {\n\t\t\t\t\t\tswitch (actions[i].type) {\n\t\t\t\t\t\t\tcase ActionType.changed:\n\t\t\t\t\t\t\tcase ActionType.createDir:\n\t\t\t\t\t\t\t\t// check if the source is the prefix of the target\n\t\t\t\t\t\t\t\tstring prefix = src ~ \"/\";\n\t\t\t\t\t\t\t\tstring target = actions[i].src;\n\t\t\t\t\t\t\t\tif (prefix[0] != '.')\n\t\t\t\t\t\t\t\t\tprefix = \"./\" ~ prefix;\n\t\t\t\t\t\t\t\tif (target[0] != '.')\n\t\t\t\t\t\t\t\t\ttarget = \"./\" ~ target;\n\t\t\t\t\t\t\t\tstring comm = commonPrefix(prefix, target);\n\t\t\t\t\t\t\t\tif (src == actions[i].src || comm.length == prefix.length) {\n\t\t\t\t\t\t\t\t\t// Hold operations require reading local file that is moved after the target is moved online\n\t\t\t\t\t\t\t\t\tpendingTargets ~= i;\n\t\t\t\t\t\t\t\t\tactions[i].skipped = true;\n\t\t\t\t\t\t\t\t\tsrcMap.remove(actions[i].src);\n\t\t\t\t\t\t\t\t\tif (comm.length == target.length)\n\t\t\t\t\t\t\t\t\t\tactions[i].src = dst;\n\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\tactions[i].src = dst ~ target[comm.length - 1 .. target.length];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\tactions ~= Action(type, false, src, dst);\n\t\tsrcMap[src] = actions.length - 1;\n\t\t\n\t\tforeach (pendingTarget; pendingTargets) {\n\t\t\tactions ~= actions[pendingTarget];\n\t\t\tactions[$-1].skipped = false;\n\t\t\tsrcMap[actions[$-1].src] = actions.length - 1;\n\t\t}\n\t}\n}\n\nfinal class Monitor {\n\t// Class variables\n\tApplicationConfig appConfig;\n\tClientSideFiltering selectiveSync;\n\n\t// Are we verbose in logging output\n\tbool verbose = false;\n\t// skip symbolic links\n\tbool skip_symlinks = false;\n\t// check for .nosync if enabled\n\tbool check_nosync = false;\n\t// check if initialised\n\tbool initialised = false;\n\t// Worker Tid\n\tTid workerTid;\n\t\n\t// Configure Private Class Variables\n\tshared(MonitorBackgroundWorker) worker;\n\t// map every inotify watch descriptor to its directory\n\tprivate string[int] wdToDirName;\n\t// map the inotify cookies of move_from events to their path\n\tprivate string[int] cookieToPath;\n\t// buffer to receive the inotify events\n\tprivate void[] buffer;\n\t\n\t// Mutex to support thread safe access of inotify watch descriptors\n\tprivate Mutex inotifyMutex;\n\n\t// Configure function delegates\n\tvoid delegate(string path) onDirCreated;\n\tvoid delegate(string[] path) onFileChanged;\n\tvoid delegate(string path) onDelete;\n\tvoid delegate(string from, string to) onMove;\n\t\n\t// List of paths that were moved, not deleted\n\tbool[string] movedNotDeleted;\n\n\t// An array of actions\n\tActionHolder actionHolder;\n\t\n\t// Configure the class variable to consume the application configuration including selective sync\n\tthis(ApplicationConfig appConfig, ClientSideFiltering selectiveSync) {\n\t\tthis.appConfig = appConfig;\n\t\tthis.selectiveSync = selectiveSync;\n\t\tinotifyMutex = new Mutex(); // Define a Mutex for thread-safe access\n\t}\n\t\n\t// The destructor should only clean up resources owned directly by this instance\n\t~this() {\n\t\tobject.destroy(worker);\n\t}\n\t\n\t// Initialise the monitor class\n\tvoid initialise() {\n\t\t// Configure the variables\n\t\tskip_symlinks = appConfig.getValueBool(\"skip_symlinks\");\n\t\tcheck_nosync = appConfig.getValueBool(\"check_nosync\");\n\t\tif (appConfig.getValueLong(\"verbose\") > 0) {\n\t\t\tverbose = true;\n\t\t}\n\t\t\n\t\tassert(onDirCreated && onFileChanged && onDelete && onMove);\n\t\tif (!buffer) buffer = new void[4096];\n\t\tworker = cast(shared) new MonitorBackgroundWorker;\n\t\tworker.initialise();\n\n\t\t// from which point do we start watching for changes?\n\t\tstring monitorPath;\n\t\tif (appConfig.getValueString(\"single_directory\") != \"\"){\n\t\t\t// single directory in use, monitor only this path\n\t\t\tmonitorPath = \"./\" ~ appConfig.getValueString(\"single_directory\");\n\t\t} else {\n\t\t\t// default \n\t\t\tmonitorPath = \".\";\n\t\t}\n\t\taddRecursive(monitorPath);\n\t\t\n\t\t// Start monitoring\n\t\tworkerTid = spawn(&startMonitorJob, worker, thisTid);\n\t\tinitialised = true;\n\t}\n\n\t// Communication with worker\n\tvoid send(bool isAlive) {\n\t\tworkerTid.send(isAlive);\n\t}\n\n\t// Shutdown the monitor class\n\tvoid shutdown() {\n\t\tif(!initialised)\n\t\t\treturn;\n\t\tinitialised = false;\n\t\t// Release all resources\n\t\tsynchronized(inotifyMutex) {\n\t\t\t// Interrupt the worker to allow removal of inotify watch descriptors\n\t\t\tworker.interrupt();\n\t\t\t// Remove all the inotify watch descriptors\n\t\t\tremoveAll();\n\t\t\t// Notify the worker that the monitor has been shutdown\n\t\t\tworker.interrupt();\n\t\t\tsend(false);\n\t\t\twdToDirName = null;\n\t\t}\n\t}\n\n\t// Recursively add this path to be monitored\n\tprivate void addRecursive(string dirname) {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t\n\t\t// skip non existing/disappeared items\n\t\tif (!exists(dirname)) {\n\t\t\tif (verboseLogging) {addLogEntry(\"Not adding non-existing/disappeared directory: \" ~ dirname, [\"verbose\"]);}\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Issue #3404: If the file is a very short lived file, and exists when the above test is done, but then is removed shortly thereafter, we need to catch this as a filesystem exception\n\t\ttry {\n\t\t\t// Skip the monitoring of any user filtered items\n\t\t\tif (dirname != \".\") {\n\t\t\t\t// Is the directory name a match to a skip_dir entry?\n\t\t\t\t// The path that needs to be checked needs to include the '/'\n\t\t\t\t// This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched\n\t\t\t\tif (isDir(dirname)) {\n\t\t\t\t\tif (selectiveSync.isDirNameExcluded(dirname.strip('.'))) {\n\t\t\t\t\t\t// dont add a watch for this item\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Skipping monitoring due to skip_dir match: \" ~ dirname, [\"debug\"]);}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (isFile(dirname)) {\n\t\t\t\t\t// Is the filename a match to a skip_file entry?\n\t\t\t\t\t// The path that needs to be checked needs to include the '/'\n\t\t\t\t\t// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched\n\t\t\t\t\tif (selectiveSync.isFileNameExcluded(dirname.strip('.'))) {\n\t\t\t\t\t\t// dont add a watch for this item\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Skipping monitoring due to skip_file match: \" ~ dirname, [\"debug\"]);}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Is the path excluded by sync_list?\n\t\t\t\tif (selectiveSync.isPathExcludedViaSyncList(buildNormalizedPath(dirname))) {\n\t\t\t\t\t// dont add a watch for this item\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Skipping monitoring parent path due to sync_list exclusion: \" ~ dirname, [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// However before we return, we need to test this path tree as a branch on this tree may be included by an anywhere exclusion rule. Do 'anywhere' inclusion rules exist?\n\t\t\t\t\tif (isDir(dirname)) {\n\t\t\t\t\t\t// Do any 'sync_list' anywhere inclusion rules exist?\n\t\t\t\t\t\tif (selectiveSync.syncListAnywhereInclusionRulesExist()) {\n\t\t\t\t\t\t\t// Yes ..\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Bypassing 'sync_list' exclusion to test if children should be monitored due to 'sync_list' anywhere rule existence\", [\"debug\"]);}\n\t\t\t\t\t\t\t// Traverse this directory\n\t\t\t\t\t\t\ttraverseDirectory(dirname);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// For the original path, we return, no inotify watch was added\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// skip symlinks if configured\n\t\t\tif (isSymlink(dirname)) {\n\t\t\t\t// if config says so we skip all symlinked items\n\t\t\t\tif (skip_symlinks) {\n\t\t\t\t\t// dont add a watch for this directory\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Do we need to check for .nosync? Only if check_nosync is true\n\t\t\tif (check_nosync) {\n\t\t\t\tif (exists(buildNormalizedPath(dirname) ~ \"/.nosync\")) {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping watching path - .nosync found & --check-for-nosync enabled: \" ~ buildNormalizedPath(dirname), [\"verbose\"]);}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isDir(dirname)) {\n\t\t\t\t// This is a directory\t\t\t\n\t\t\t\t// is the path excluded if skip_dotfiles configured and path is a .folder?\n\t\t\t\tif ((selectiveSync.getSkipDotfiles()) && (isDotFile(dirname))) {\n\t\t\t\t\t// dont add a watch for this directory\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// passed all potential exclusions\n\t\t\t// add inotify watch for this path / directory / file\n\t\t\tif (debugLogging) {addLogEntry(\"Calling worker.addInotifyWatch() for this dirname: \" ~ dirname, [\"debug\"]);}\n\t\t\tint wd = worker.addInotifyWatch(dirname);\n\t\t\tif (wd > 0) {\n\t\t\t\twdToDirName[wd] = buildNormalizedPath(dirname) ~ \"/\";\n\t\t\t}\n\t\t\t\n\t\t\t// if this is a directory, recursively add this path\n\t\t\tif (isDir(dirname)) {\n\t\t\t\ttraverseDirectory(dirname);\n\t\t\t}\n\t\t// Catch any FileException error which is generated\n\t\t} catch (std.file.FileException e) {\n\t\t\t// Standard filesystem error\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, dirname);\n\t\t\treturn;\n\t\t}\n\t}\n\t\n\t// Traverse directory to test if this should have an inotify watch added\n\tprivate void traverseDirectory(string dirname) {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\t// Current path for error logging\n\t\tstring currentPath;\n\t\t\n\t\t// Try and get all the directory entities for this path\n\t\ttry {\n\t\t\tauto pathList = dirEntries(dirname, SpanMode.shallow, false);\n\t\t\tforeach(DirEntry entry; pathList) {\n\t\t\t\tcurrentPath = entry.name;\n\t\t\t\tif (entry.isDir) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Calling addRecursive() for this directory: \" ~ entry.name, [\"debug\"]);}\n\t\t\t\t\taddRecursive(entry.name);\n\t\t\t\t}\n\t\t\t}\n\t\t// Catch any FileException error which is generated\n\t\t} catch (std.file.FileException e) {\n\t\t\t// Standard filesystem error\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, currentPath);\n\t\t\treturn;\n\t\t} catch (Exception e) {\n\t\t\t// Issue #1154 handling\n\t\t\t// Need to check for: Failed to stat file in error message\n\t\t\tif (canFind(e.msg, \"Failed to stat file\")) {\n\t\t\t\t// File system access issue\n\t\t\t\taddLogEntry(\"ERROR: The local file system returned an error with the following message:\");\n\t\t\t\taddLogEntry(\"  Error Message: \" ~ e.msg);\n\t\t\t\taddLogEntry(\"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\");\n\t\t\t\taddLogEntry(\"\\nFATAL: Forcing exiting application to avoid deleting data due to local file system access issues\\n\");\n\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\tforceExit();\n\t\t\t} else {\n\t\t\t\t// some other error\n\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, currentPath);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove a watch descriptor\n\tprivate void removeAll() {\n\t\tstring[int] copy;\n\t\tsynchronized(inotifyMutex) {\n\t\t\tcopy = wdToDirName.dup; // Make a thread-safe copy\n\t\t}\n\n\t\t// Loop through the watch descriptors and remove\n\t\tforeach (wd, path; copy) {\n\t\t\tremove(wd);\n\t\t}\n\t}\n\n\tprivate void remove(int wd) {\n\t\tassert(wd in wdToDirName);\n\t\t\n\t\tsynchronized(inotifyMutex) {\n\t\t\tint ret = worker.removeInotifyWatch(wd);\n\t\t\tif (ret < 0) throw new MonitorException(\"inotify_rm_watch failed\");\n\t\t\tif (verboseLogging) {addLogEntry(\"Stopped monitoring directory (inotify watch removed): \" ~ to!string(wdToDirName[wd]), [\"verbose\"]);}\n\t\t\twdToDirName.remove(wd);\n\t\t}\n\t}\n\n\t// Remove the watch descriptors associated to the given path\n\tprivate void remove(const(char)[] path) {\n\t\tpath ~= \"/\";\n\t\tforeach (wd, dirname; wdToDirName) {\n\t\t\tif (dirname.startsWith(path)) {\n\t\t\t\tint ret = worker.removeInotifyWatch(wd);\n\t\t\t\tif (ret < 0) throw new MonitorException(\"inotify_rm_watch failed\");\n\t\t\t\twdToDirName.remove(wd);\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Stopped monitoring directory (inotify watch removed): \" ~ dirname, [\"verbose\"]);}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the file path from an inotify event\n\tprivate string getPath(const(inotify_event)* event) {\n\t\tstring path = wdToDirName[event.wd];\n\t\tif (event.len > 0) path ~= fromStringz(event.name.ptr);\n\t\tif (debugLogging) {addLogEntry(\"inotify path event for: \" ~ path, [\"debug\"]);}\n\t\treturn path;\n\t}\n\n\t// Update\n\tvoid update(bool useCallbacks = true) {\n\t\tif(!initialised)\n\t\t\treturn;\n\t\n\t\tpollfd fds = {\n\t\t\tfd: worker.fd,\n\t\t\tevents: POLLIN\n\t\t};\n\n\t\twhile (true) {\n\t\t\tbool hasNotification = false;\n\t\t\tint sleep_counter = 0;\n\t\t\t// Batch events up to 5 seconds\n\t\t\twhile (sleep_counter < 5) {\n\t\t\t\tint ret = poll(&fds, 1, 0);\n\t\t\t\tif (ret == -1) throw new MonitorException(\"poll failed\");\n\t\t\t\telse if (ret == 0) break; // no events available\n\t\t\t\thasNotification = true;\n\t\t\t\tsize_t length = read(worker.fd, buffer.ptr, buffer.length);\n\t\t\t\tif (length == -1) throw new MonitorException(\"read failed\");\n\n\t\t\t\tint i = 0;\n\t\t\t\twhile (i < length) {\n\t\t\t\t\tinotify_event *event = cast(inotify_event*) &buffer[i];\n\t\t\t\t\tstring path;\n\t\t\t\t\tstring evalPath;\n\t\t\t\t\t\n\t\t\t\t\t// inotify event debug\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"inotify event wd: \" ~ to!string(event.wd), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"inotify event mask: \" ~ to!string(event.mask), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"inotify event cookie: \" ~ to!string(event.cookie), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"inotify event len: \" ~ to!string(event.len), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"inotify event name: \" ~ to!string(event.name), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// inotify event handling\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\tif (event.mask & IN_ACCESS) addLogEntry(\"inotify event flag: IN_ACCESS\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_MODIFY) addLogEntry(\"inotify event flag: IN_MODIFY\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_ATTRIB) addLogEntry(\"inotify event flag: IN_ATTRIB\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_CLOSE_WRITE) addLogEntry(\"inotify event flag: IN_CLOSE_WRITE\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_CLOSE_NOWRITE) addLogEntry(\"inotify event flag: IN_CLOSE_NOWRITE\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_MOVED_FROM) addLogEntry(\"inotify event flag: IN_MOVED_FROM\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_MOVED_TO) addLogEntry(\"inotify event flag: IN_MOVED_TO\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_CREATE) addLogEntry(\"inotify event flag: IN_CREATE\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_DELETE) addLogEntry(\"inotify event flag: IN_DELETE\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_DELETE_SELF) addLogEntry(\"inotify event flag: IN_DELETE_SELF\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_MOVE_SELF) addLogEntry(\"inotify event flag: IN_MOVE_SELF\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_UNMOUNT) addLogEntry(\"inotify event flag: IN_UNMOUNT\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_Q_OVERFLOW) addLogEntry(\"inotify event flag: IN_Q_OVERFLOW\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_IGNORED) addLogEntry(\"inotify event flag: IN_IGNORED\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_CLOSE) addLogEntry(\"inotify event flag: IN_CLOSE\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_MOVE) addLogEntry(\"inotify event flag: IN_MOVE\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_ONLYDIR) addLogEntry(\"inotify event flag: IN_ONLYDIR\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_DONT_FOLLOW) addLogEntry(\"inotify event flag: IN_DONT_FOLLOW\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_EXCL_UNLINK) addLogEntry(\"inotify event flag: IN_EXCL_UNLINK\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_MASK_ADD) addLogEntry(\"inotify event flag: IN_MASK_ADD\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_ISDIR) addLogEntry(\"inotify event flag: IN_ISDIR\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_ONESHOT) addLogEntry(\"inotify event flag: IN_ONESHOT\", [\"debug\"]);\n\t\t\t\t\t\tif (event.mask & IN_ALL_EVENTS) addLogEntry(\"inotify event flag: IN_ALL_EVENTS\", [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// skip events that need to be ignored\n\t\t\t\t\tif (event.mask & IN_IGNORED) {\n\t\t\t\t\t\t// forget the directory associated to the watch descriptor\n\t\t\t\t\t\twdToDirName.remove(event.wd);\n\t\t\t\t\t\tgoto skip;\n\t\t\t\t\t} else if (event.mask & IN_Q_OVERFLOW) {\n\t\t\t\t\t\tthrow new MonitorException(\"inotify queue overflow: some events may be lost\");\n\t\t\t\t\t}\n\n\t\t\t\t\t// if the event is not to be ignored, obtain path\n\t\t\t\t\tpath = getPath(event);\n\t\t\t\t\t// configure the skip_dir & skip skip_file comparison item\n\t\t\t\t\tevalPath = path.strip('.');\n\t\t\t\t\t\n\t\t\t\t\t// Skip events that should be excluded based on application configuration\n\t\t\t\t\t// We cant use isDir or isFile as this information is missing from the inotify event itself\n\t\t\t\t\t// Thus this causes a segfault when attempting to query this - https://github.com/abraunegg/onedrive/issues/995\n\t\t\t\t\t\n\t\t\t\t\t// Based on the 'type' of event & object type (directory or file) check that path against the 'right' user exclusions\n\t\t\t\t\t// Directory events should only be compared against skip_dir and file events should only be compared against skip_file\n\t\t\t\t\tif (event.mask & IN_ISDIR) {\n\t\t\t\t\t\t// The event in question contains IN_ISDIR event mask, thus highly likely this is an event on a directory\n\t\t\t\t\t\t// This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched\n\t\t\t\t\t\tif (selectiveSync.isDirNameExcluded(evalPath)) {\n\t\t\t\t\t\t\t// The path to evaluate matches a path that the user has configured to skip\n\t\t\t\t\t\t\tgoto skip;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// The event in question missing the IN_ISDIR event mask, thus highly likely this is an event on a file\n\t\t\t\t\t\t// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched\n\t\t\t\t\t\tif (selectiveSync.isFileNameExcluded(evalPath)) {\n\t\t\t\t\t\t\t// The path to evaluate matches a file that the user has configured to skip\n\t\t\t\t\t\t\tgoto skip;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// is the path, excluded via sync_list\n\t\t\t\t\tif (selectiveSync.isPathExcludedViaSyncList(path)) {\n\t\t\t\t\t\t// The path to evaluate matches a directory or file that the user has configured not to include in the sync\n\t\t\t\t\t\tgoto skip;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// handle the inotify events\n\t\t\t\t\tif (event.mask & IN_MOVED_FROM) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"event IN_MOVED_FROM: \" ~ path, [\"debug\"]);}\n\t\t\t\t\t\tcookieToPath[event.cookie] = path;\n\t\t\t\t\t\tmovedNotDeleted[path] = true; // Mark as moved, not deleted\n\t\t\t\t\t} else if (event.mask & IN_MOVED_TO) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"event IN_MOVED_TO: \" ~ path, [\"debug\"]);}\n\t\t\t\t\t\tif (event.mask & IN_ISDIR) addRecursive(path);\n\t\t\t\t\t\tauto from = event.cookie in cookieToPath;\n\t\t\t\t\t\tif (from) {\n\t\t\t\t\t\t\tcookieToPath.remove(event.cookie);\n\t\t\t\t\t\t\tif (useCallbacks) actionHolder.append(ActionType.moved, *from, path);\n\t\t\t\t\t\t\tmovedNotDeleted.remove(*from); // Clear moved status\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Handle file moved in from outside\n\t\t\t\t\t\t\tif (event.mask & IN_ISDIR) {\n\t\t\t\t\t\t\t\tif (useCallbacks) actionHolder.append(ActionType.createDir, path);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (useCallbacks) actionHolder.append(ActionType.changed, path);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.mask & IN_CREATE) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"event IN_CREATE: \" ~ path, [\"debug\"]);}\n\t\t\t\t\t\tif (event.mask & IN_ISDIR) {\n\t\t\t\t\t\t\t// fix from #2586\n\t\t\t\t\t\t\tauto cookieToPath1 = cookieToPath.dup();\n\t\t\t\t\t\t\tforeach (cookie, path1; cookieToPath1) {\n\t\t\t\t\t\t\t\tif (path1 == path) {\n\t\t\t\t\t\t\t\t\tcookieToPath.remove(cookie);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\taddRecursive(path);\n\t\t\t\t\t\t\tif (useCallbacks) actionHolder.append(ActionType.createDir, path);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.mask & IN_DELETE) {\n\t\t\t\t\t\tif (path in movedNotDeleted) {\n\t\t\t\t\t\t\tmovedNotDeleted.remove(path); // Ignore delete for moved files\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"event IN_DELETE: \" ~ path, [\"debug\"]);}\n\t\t\t\t\t\t\tif (useCallbacks) actionHolder.append(ActionType.deleted, path);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if ((event.mask & IN_CLOSE_WRITE) && !(event.mask & IN_ISDIR)) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"event IN_CLOSE_WRITE and not IN_ISDIR: \" ~ path, [\"debug\"]);}\n\t\t\t\t\t\t// fix from #2586\n\t\t\t\t\t\tauto cookieToPath1 = cookieToPath.dup();\n\t\t\t\t\t\tforeach (cookie, path1; cookieToPath1) {\n\t\t\t\t\t\t\tif (path1 == path) {\n\t\t\t\t\t\t\t\tcookieToPath.remove(cookie);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (useCallbacks) actionHolder.append(ActionType.changed, path);\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"inotify event unhandled: \" ~ path);\n\t\t\t\t\t\tassert(0);\n\t\t\t\t\t}\n\n\t\t\t\t\tskip:\n\t\t\t\t\ti += inotify_event.sizeof + event.len;\n\t\t\t\t}\n\n\t\t\t\t// Sleep for one second to prevent missing fast-changing events.\n\t\t\t\tif (poll(&fds, 1, 0) == 0) {\n\t\t\t\t\tsleep_counter += 1;\n\t\t\t\t\tThread.sleep(dur!\"seconds\"(1));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!hasNotification) break;\n\t\t\tprocessChanges();\n\n\t\t\t// Assume that the items moved outside the watched directory have been deleted\n\t\t\tforeach (cookie, path; cookieToPath) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Deleting cookie|watch (post loop): \" ~ path, [\"debug\"]);}\n\t\t\t\tif (useCallbacks) onDelete(path);\n\t\t\t\tremove(path);\n\t\t\t\tcookieToPath.remove(cookie);\n\t\t\t}\n\t\t\t// Debug Log that all inotify events are flushed\n\t\t\tif (debugLogging) {addLogEntry(\"inotify events flushed\", [\"debug\"]);}\n\t\t}\n\t}\n  \n\tprivate void processChanges() {\n\t\tstring[] changes;\n\n\t\tforeach(action; actionHolder.actions) {\n\t\t\tif (action.skipped)\n\t\t\t\tcontinue;\n\t\t\tswitch (action.type) {\n\t\t\t\tcase ActionType.changed:\n\t\t\t\t\tchanges ~= action.src;\n\t\t\t\t\tbreak;\n\t\t\t\tcase ActionType.deleted:\n\t\t\t\t\tonDelete(action.src);\n\t\t\t\t\tbreak;\n\t\t\t\tcase ActionType.createDir:\n\t\t\t\t\tonDirCreated(action.src);\n\t\t\t\t\tbreak;\n\t\t\t\tcase ActionType.moved:\n\t\t\t\t\tonMove(action.src, action.dst);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!changes.empty) {\n\t\t\tonFileChanged(changes);\n\t\t}\n\n\t\tobject.destroy(actionHolder);\n\t}\n}\n"
  },
  {
    "path": "src/notifications/README",
    "content": "The files in this directory have been obtained form the following places:\n\ndnotify.d\n\thttps://github.com/Dav1dde/dnotify/blob/master/dnotify.d\n\tLicense: Creative Commons Zro 1.0 Universal\n\tsee https://github.com/Dav1dde/dnotify/blob/master/LICENSE\n\nnotify.d\n\thttps://github.com/D-Programming-Deimos/libnotify/blob/master/deimos/notify/notify.d\n\tLicense: GNU Lesser General Public License (LGPL) 2.1 or upwards, see file\n"
  },
  {
    "path": "src/notifications/dnotify.d",
    "content": "module dnotify;\n\nimport std.path;\nimport std.file;\n\nprivate {\n    import std.string : toStringz;\n    import std.conv : to;\n    import std.traits : isPointer, isArray;\n    import std.variant : Variant;\n    import std.array : appender;\n    import deimos.notify.notify;\n}\n\npublic import deimos.notify.notify : NOTIFY_EXPIRES_DEFAULT, NOTIFY_EXPIRES_NEVER,\n                                     NotifyUrgency;\n\nversion(NoPragma) {\n} else {\n    pragma(lib, \"notify\");\n    pragma(lib, \"gmodule\");\n    pragma(lib, \"glib-2.0\");\n}\n\nextern (C) {\n    private void g_free(void* mem);\n    private void g_list_free(GList* glist);\n}\n\nversion(NoGdk) {\n} else {\n    version(NoPragma) {\n    } else {\n        pragma(lib, \"gdk_pixbuf\");\n    }\n\n    private:\n    extern (C) {\n        GdkPixbuf* gdk_pixbuf_new_from_file(const(char)* filename, GError **error);\n    }\n}\n\nclass NotificationError : Exception {\n    string message;\n    GError* gerror;\n    \n    this(GError* gerror) {\n        this.message = to!(string)(gerror.message);\n        this.gerror = gerror;\n        \n        super(this.message);\n    }\n\n    this(string message) {\n        this.message = message;\n\n        super(message);\n    }\n}\n\nbool check_availability() {\n    // notify_init might return without dbus server actually started\n    // try to check for running dbus server\n    char **ret_name;\n    char **ret_vendor;\n    char **ret_version;\n    char **ret_spec_version;\n    bool ret;\n    try {\n\treturn notify_get_server_info(ret_name, ret_vendor, ret_version, ret_spec_version);\n    } catch (NotificationError e) {\n\tthrow new NotificationError(\"Cannot find dbus server!\");\n    }\n}\n\nvoid init(in char[] name) {\n    notify_init(name.toStringz());\n}\n\nalias notify_is_initted is_initted;\nalias notify_uninit uninit;\n\nshared static this() {\n\tinit(baseName(thisExePath()));\n}\n\nshared static ~this() {\n    uninit();\n}\n\nstring get_app_name() {\n    return to!(string)(notify_get_app_name());\n}\n\nvoid set_app_name(in char[] app_name) {\n    notify_set_app_name(app_name.toStringz());\n}\n\nstring[] get_server_caps() {\n    auto result = appender!(string[])();\n    \n    GList* list = notify_get_server_caps();\n    if(list !is null) {\n        for(GList* c = list; c !is null; c = c.next) {\n            result.put(to!(string)(cast(char*)c.data));\n            g_free(c.data);\n        }\n\n        g_list_free(list);\n    }\n\n    return result.data;\n}\n\nstruct ServerInfo {\n    string name;\n    string vendor;\n    string version_;\n    string spec_version;\n}\n\nServerInfo get_server_info() {\n    char* name;\n    char* vendor;\n    char* version_;\n    char* spec_version;\n    notify_get_server_info(&name, &vendor, &version_, &spec_version);\n\n    scope(exit) {\n        g_free(name);\n        g_free(vendor);\n        g_free(version_);\n        g_free(spec_version);\n    }\n\n    return ServerInfo(to!string(name), to!string(vendor), to!string(version_), to!string(spec_version));\n}\n\n\nstruct Action {\n    const(char[]) id;\n    const(char[]) label;\n    NotifyActionCallback callback;\n    void* user_ptr;\n}\n\n\nclass Notification {\n    NotifyNotification* notify_notification;\n    \n    const(char)[] summary;\n    const(char)[] body_;\n    const(char)[] icon;\n\n    bool closed = true;\n    \n    private int _timeout = NOTIFY_EXPIRES_DEFAULT;\n    const(char)[] _category;\n    NotifyUrgency _urgency;\n    GdkPixbuf* _image;\n    Variant[const(char)[]] _hints;\n    const(char)[] _app_name;\n    Action[] _actions;\n\n    this(in char[] summary, in char[] body_, in char[] icon=\"\")\n        in { assert(is_initted(), \"call dnotify.init() before using Notification\"); }\n        do {\n            this.summary = summary;\n            this.body_ = body_;\n            this.icon = icon;\n            notify_notification = notify_notification_new(summary.toStringz(), body_.toStringz(), icon.toStringz());\n        }\n\n    bool update(in char[] summary, in char[] body_, in char[] icon=\"\") {\n        this.summary = summary;\n        this.body_ = body_;\n        this.icon = icon;\n        return notify_notification_update(notify_notification, summary.toStringz(), body_.toStringz(), icon.toStringz());\n    }\n\n    void show() {\n        GError* ge;\n\n        if(!notify_notification_show(notify_notification, &ge)) {\n            throw new NotificationError(ge);\n        }\n    }\n\n    @property int timeout() { return _timeout; }\n    @property void timeout(int timeout) {\n        this._timeout = timeout;\n        notify_notification_set_timeout(notify_notification, timeout);\n    }\n\n    @property const(char[]) category() { return _category; }\n    @property void category(in char[] category) {\n        this._category = category;\n        notify_notification_set_category(notify_notification, category.toStringz());\n    }\n\n    @property NotifyUrgency urgency() { return _urgency; }\n    @property void urgency(NotifyUrgency urgency) {\n        this._urgency = urgency;\n        notify_notification_set_urgency(notify_notification, urgency);\n    }\n\n\n    void set_image(GdkPixbuf* pixbuf) {\n        notify_notification_set_image_from_pixbuf(notify_notification, pixbuf);\n        //_image = pixbuf;\n    }\n    \n    version(NoGdk) {\n    } else {\n        void set_image(in char[] filename) { \n            GError* ge;\n            // TODO: free pixbuf\n            GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(filename.toStringz(), &ge);\n\n            if(pixbuf is null) {\n                if(ge is null) {\n                    throw new NotificationError(\"Unable to load file: \" ~ filename.idup);\n                } else {\n                    throw new NotificationError(ge);\n                }\n            }\n            assert(notify_notification !is null);\n            notify_notification_set_image_from_pixbuf(notify_notification, pixbuf); // TODO: fix segfault\n            //_image = pixbuf;\n        }\n    }\n\n    @property GdkPixbuf* image() { return _image; }\n    \n    // using deprecated set_hint_* functions (GVariant is an opaque structure, which needs the glib)\n    void set_hint(T)(in char[] key, T value) {\n        static if(is(T == int)) {\n            notify_notification_set_hint_int32(notify_notification, key, value);\n        } else static if(is(T == uint)) {\n            notify_notification_set_hint_uint32(notify_notification, key, value);\n        } else static if(is(T == double)) {\n            notify_notification_set_hint_double(notify_notification, key, value);\n        } else static if(is(T : const(char)[])) {\n            notify_notification_set_hint_string(notify_notification, key, value.toStringz());\n        } else static if(is(T == ubyte)) {\n            notify_notification_set_hint_byte(notify_notification, key, value);\n        } else static if(is(T == ubyte[])) {\n            notify_notification_set_hint_byte_array(notify_notification, key, value.ptr, value.length);\n        } else {\n            static assert(false, \"unsupported value for Notification.set_hint\");\n        }\n\n        _hints[key] = Variant(value);\n    }\n\n    // unset hint?\n\n    Variant get_hint(in char[] key) {\n        return _hints[key];\n    }\n\n    @property const(char)[] app_name() { return _app_name; }\n    @property void app_name(in char[] name) {\n        this._app_name = app_name;\n        notify_notification_set_app_name(notify_notification, app_name.toStringz());\n    }\n\n    void add_action(T)(in char[] action, in char[] label, NotifyActionCallback callback, T user_data) {\n        static if(isPointer!T) {\n            void* user_ptr = cast(void*)user_data;\n        } else static if(isArray!T) {\n            void* user_ptr = cast(void*)user_data.ptr;\n        } else {\n            void* user_ptr = cast(void*)&user_data;\n        }\n\n        notify_notification_add_action(notify_notification, action.toStringz(), label.toStringz(),\n                                       callback, user_ptr, null);\n\n        _actions ~= Action(action, label, callback, user_ptr);\n    }\n\n    void add_action()(Action action) {\n        notify_notification_add_action(notify_notification, action.id.toStringz(), action.label.toStringz(),\n                                       action.callback, action.user_ptr, null);\n\n        _actions ~= action;\n    }\n\n    @property Action[] actions() { return _actions; }\n    \n    void clear_actions() {\n        notify_notification_clear_actions(notify_notification);\n    }\n\n    void close() {\n        GError* ge;\n        \n        if(!notify_notification_close(notify_notification, &ge)) {\n            throw new NotificationError(ge);\n        }\n    }\n\n    @property int closed_reason() {\n        return notify_notification_get_closed_reason(notify_notification);\n    }\n}\n\n\nversion(TestMain) {\n    import std.stdio;\n    \n    void main() {\n        writeln(get_app_name());\n        set_app_name(\"blargh\");\n        writeln(get_app_name());\n        writeln(get_server_caps());\n        writeln(get_server_info());\n        \n        auto n = new Notification(\"foo\", \"bar\", \"notification-message-im\");\n        n.timeout = 3;\n        n.show();\n    }\n}\n"
  },
  {
    "path": "src/notifications/notify.d",
    "content": "/**\n * Copyright (C) 2004-2006 Christian Hammond\n * Copyright (C) 2010 Red Hat, Inc.\n *\n * This library is free software; you can redistribute it and/or\n * modify it under the terms of the GNU Lesser General Public\n * License as published by the Free Software Foundation; either\n * version 2.1 of the License, or (at your option) any later version.\n *\n * This library is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n * Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public\n * License along with this library; if not, write to the\n * Free Software Foundation, Inc., 59 Temple Place - Suite 330,\n * Boston, MA  02111-1307, USA.\n */\n\nmodule deimos.notify.notify;\n\n\nenum NOTIFY_VERSION_MAJOR = 0;\nenum NOTIFY_VERSION_MINOR = 7;\nenum NOTIFY_VERSION_MICRO = 5;\n\ntemplate NOTIFY_CHECK_VERSION(int major, int minor, int micro) {\n    enum NOTIFY_CHECK_VERSION = ((NOTIFY_VERSION_MAJOR > major) ||\n            (NOTIFY_VERSION_MAJOR == major && NOTIFY_VERSION_MINOR > minor) ||\n            (NOTIFY_VERSION_MAJOR == major && NOTIFY_VERSION_MINOR == minor &&\n             NOTIFY_VERSION_MICRO >= micro));\n}\n\n\nalias ulong GType;\nalias void function(void*) GFreeFunc;\n\nstruct GError {\n  uint domain;\n  int code;\n  char* message;\n}\n\nstruct GList {\n  void* data;\n  GList* next;\n  GList* prev;\n}\n\n// dummies\nstruct GdkPixbuf {}\nstruct GObject {}\nstruct GObjectClass {}\nstruct GVariant {}\n\nGType notify_urgency_get_type();\n\n/**\n * NOTIFY_EXPIRES_DEFAULT:\n *\n * The default expiration time on a notification.\n */\nenum NOTIFY_EXPIRES_DEFAULT = -1;\n\n/**\n * NOTIFY_EXPIRES_NEVER:\n *\n * The notification never expires. It stays open until closed by the calling API\n * or the user.\n */\nenum NOTIFY_EXPIRES_NEVER = 0;\n\n// #define NOTIFY_TYPE_NOTIFICATION         (notify_notification_get_type ())\n// #define NOTIFY_NOTIFICATION(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), NOTIFY_TYPE_NOTIFICATION, NotifyNotification))\n// #define NOTIFY_NOTIFICATION_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), NOTIFY_TYPE_NOTIFICATION, NotifyNotificationClass))\n// #define NOTIFY_IS_NOTIFICATION(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), NOTIFY_TYPE_NOTIFICATION))\n// #define NOTIFY_IS_NOTIFICATION_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), NOTIFY_TYPE_NOTIFICATION))\n// #define NOTIFY_NOTIFICATION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), NOTIFY_TYPE_NOTIFICATION, NotifyNotificationClass))\n\nextern (C) {\n    struct NotifyNotificationPrivate;\n    \n    struct NotifyNotification {\n            /*< private >*/\n            GObject      parent_object;\n\n            NotifyNotificationPrivate *priv;\n    }\n\n    struct NotifyNotificationClass {\n            GObjectClass    parent_class;\n\n            /* Signals */\n            void function(NotifyNotification *notification) closed;\n    }\n\n\n    /**\n     * NotifyUrgency:\n     * @NOTIFY_URGENCY_LOW: Low urgency. Used for unimportant notifications.\n     * @NOTIFY_URGENCY_NORMAL: Normal urgency. Used for most standard notifications.\n     * @NOTIFY_URGENCY_CRITICAL: Critical urgency. Used for very important notifications.\n     *\n     * The urgency level of the notification.\n     */\n    enum NotifyUrgency {\n            NOTIFY_URGENCY_LOW,\n            NOTIFY_URGENCY_NORMAL,\n            NOTIFY_URGENCY_CRITICAL,\n\n    }\n\n    /**\n     * NotifyActionCallback:\n     * @notification:\n     * @action:\n     * @user_data:\n     *\n     * An action callback function.\n     */\n    alias void function(NotifyNotification* notification, char* action, void* user_data) NotifyActionCallback;\n\n\n    GType notify_notification_get_type();\n\n    NotifyNotification* notify_notification_new(const(char)* summary, const(char)* body_, const(char)* icon);\n\n    bool notify_notification_update(NotifyNotification* notification, const(char)* summary, const(char)* body_, const(char)* icon);\n\n    bool notify_notification_show(NotifyNotification* notification, GError** error);\n\n    void notify_notification_set_timeout(NotifyNotification* notification, int timeout);\n\n    void notify_notification_set_category(NotifyNotification* notification, const(char)* category);\n\n    void notify_notification_set_urgency(NotifyNotification* notification, NotifyUrgency urgency);\n\n    void notify_notification_set_image_from_pixbuf(NotifyNotification* notification, GdkPixbuf* pixbuf);\n\n    void notify_notification_set_icon_from_pixbuf(NotifyNotification* notification, GdkPixbuf* icon);\n\n    void notify_notification_set_hint_int32(NotifyNotification* notification, const(char)* key, int value);\n    void notify_notification_set_hint_uint32(NotifyNotification* notification, const(char)* key, uint value);\n\n    void notify_notification_set_hint_double(NotifyNotification* notification, const(char)* key, double value);\n\n    void notify_notification_set_hint_string(NotifyNotification* notification, const(char)* key, const(char)* value);\n\n    void notify_notification_set_hint_byte(NotifyNotification* notification, const(char)* key, ubyte value);\n\n    void notify_notification_set_hint_byte_array(NotifyNotification* notification, const(char)* key, const(ubyte)* value, ulong len);\n\n    void notify_notification_set_hint(NotifyNotification* notification, const(char)* key, GVariant* value);\n\n    void notify_notification_set_app_name(NotifyNotification* notification, const(char)* app_name);\n\n    void notify_notification_clear_hints(NotifyNotification* notification);\n\n    void notify_notification_add_action(NotifyNotification* notification, const(char)* action, const(char)* label,\n                                        NotifyActionCallback callback, void* user_data, GFreeFunc free_func);\n\n    void notify_notification_clear_actions(NotifyNotification* notification);\n    bool notify_notification_close(NotifyNotification* notification, GError** error);\n\n    int notify_notification_get_closed_reason(const NotifyNotification* notification);\n\n\n\n    bool notify_init(const(char)* app_name);\n    void notify_uninit();\n    bool notify_is_initted();\n\n    const(char)* notify_get_app_name();\n    void notify_set_app_name(const(char)* app_name);\n\n    GList *notify_get_server_caps();\n\n    bool notify_get_server_info(char** ret_name, char** ret_vendor, char** ret_version, char** ret_spec_version);\n}\n\nversion(MainTest) {\n    import std.string;\n    \n    void main() {\n        \n        notify_init(\"test\".toStringz());\n\n        auto n = notify_notification_new(\"summary\".toStringz(), \"body\".toStringz(), \"none\".toStringz());\n        GError* ge;\n        notify_notification_show(n, &ge);\n        \n        scope(success) notify_uninit();\n    }\n}\n"
  },
  {
    "path": "src/onedrive.d",
    "content": "// What is this module called?\nmodule onedrive;\n\n// What does this module require to function?\nimport core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;\nimport core.memory;\nimport core.thread;\nimport std.stdio;\nimport std.string;\nimport std.utf;\nimport std.file;\nimport std.exception;\nimport std.regex;\nimport std.json;\nimport std.algorithm;\nimport std.net.curl;\nimport std.datetime;\nimport std.path;\nimport std.conv;\nimport std.math;\nimport std.uri;\nimport std.array;\n\n// Required for webhooks\nimport std.uuid;\n\n// What other modules that we have created do we need to import?\nimport config;\nimport log;\nimport util;\nimport curlEngine;\nimport intune;\n\n// Define the 'OneDriveException' class\nclass OneDriveException : Exception {\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/errors\n\tint httpStatusCode;\n\tconst CurlResponse response;\n\tprivate JSONValue _error;\n\n\t// Public property to access the JSON error\n\t@property JSONValue error() const {\n\t\treturn _error;\n\t}\n\n\tthis(int httpStatusCode, string reason, const CurlResponse response, string file = __FILE__, size_t line = __LINE__) {\n\t\tthis.httpStatusCode = httpStatusCode;\n\t\tthis.response = response;\n\t\tthis._error = response.json();\n\t\tstring msg = format(\"HTTP request returned status code %d (%s)\\n%s\", httpStatusCode, reason, toJSON(_error, true));\n\t\tsuper(msg, file, line);\n\t}\n\n\tthis(int httpStatusCode, string reason, string file = __FILE__, size_t line = __LINE__) {\n\t\tthis.httpStatusCode = httpStatusCode;\n\t\tthis.response = null;\n\t\tsuper(msg, file, line);\n\t}\n}\n\n// Define the 'OneDriveError' class\nclass OneDriveError: Error {\n\tthis(string msg) {\n\t\tsuper(msg);\n\t}\n}\n\n// Define the 'OneDriveApi' class\nclass OneDriveApi {\n\t// Class variables that use other classes\n\tApplicationConfig appConfig;\n\tCurlEngine curlEngine;\n\tCurlResponse response;\n\t\n\t// API Endpoint Constants\n\timmutable string defaultDriveUrlAPIEndpoint = \"/v1.0/me/drive\";\n\timmutable string defaultDriveByIdUrlAPIEndpoint = \"/v1.0/drives/\";\n\timmutable string defaultSharedWithMeUrlAPIEndpoint = \"/v1.0/me/drive/sharedWithMe\";\n\timmutable string defaultItemByIdUrlAPIEndpoint = \"/v1.0/me/drive/items/\";\n\timmutable string defaultItemByPathUrlAPIEndpoint = \"/v1.0/me/drive/root:/\";\n\timmutable string defaultSiteSearchUrlAPIEndpoint = \"/v1.0/sites?search\";\n\timmutable string defaultSiteDriveUrlAPIEndpoint = \"/v1.0/sites/\";\n\timmutable string defaultSubscriptionUrlAPIEndpoint = \"/v1.0/subscriptions\";\n\timmutable string defaultWebsocketEndpointAPIEndpoint = \"/v1.0/me/drive/root/subscriptions/socketIo\";\n\t\n\t// Class variables\n\tstring clientId = \"\";\n\tstring companyName = \"\";\n\tstring authUrl = \"\";\n\tstring deviceAuthUrl = \"\";\n\tstring redirectUrl = \"\";\n\tstring tokenUrl = \"\";\n\tstring driveUrl = \"\";\n\tstring driveByIdUrl = \"\";\n\tstring sharedWithMeUrl = \"\";\n\tstring itemByIdUrl = \"\";\n\tstring itemByPathUrl = \"\";\n\tstring siteSearchUrl = \"\";\n\tstring siteDriveUrl = \"\";\n\tstring subscriptionUrl = \"\";\n\tstring tenantId = \"\";\n\tstring authScope = \"\";\n\tstring websocketEndpoint = \"\";\n\tstring websocketEndpointAPIEndpoint = defaultWebsocketEndpointAPIEndpoint;\n\tconst(char)[] refreshToken = \"\";\n\tbool dryRun = false;\n\tbool keepAlive = false;\n\n\tthis(ApplicationConfig appConfig) {\n\t\t// Configure the class variable to consume the application configuration\n\t\tthis.appConfig = appConfig;\n\t\tthis.curlEngine = null;\n\t\tthis.response = null;\n\t\t// Configure the major API Query URL's, based on using application configuration\n\t\t// These however can be updated by config option 'azure_ad_endpoint', thus handled differently\n\t\t\n\t\t// Drive Queries\n\t\tdriveUrl = appConfig.globalGraphEndpoint ~ defaultDriveUrlAPIEndpoint;\n\t\tdriveByIdUrl = appConfig.globalGraphEndpoint ~ defaultDriveByIdUrlAPIEndpoint;\n\n\t\t// What is 'shared with me' Query\n\t\tsharedWithMeUrl = appConfig.globalGraphEndpoint ~ defaultSharedWithMeUrlAPIEndpoint;\n\n\t\t// Item Queries\n\t\titemByIdUrl = appConfig.globalGraphEndpoint ~ defaultItemByIdUrlAPIEndpoint;\n\t\titemByPathUrl = appConfig.globalGraphEndpoint ~ defaultItemByPathUrlAPIEndpoint;\n\n\t\t// Office 365 / SharePoint Queries\n\t\tsiteSearchUrl = appConfig.globalGraphEndpoint ~ defaultSiteSearchUrlAPIEndpoint;\n\t\tsiteDriveUrl = appConfig.globalGraphEndpoint ~ defaultSiteDriveUrlAPIEndpoint;\n\n\t\t// Subscriptions\n\t\tsubscriptionUrl = appConfig.globalGraphEndpoint ~ defaultSubscriptionUrlAPIEndpoint;\n\t\t\n\t\t// WebSocket Endpoint - sets the default: /v1.0/me/drive/root/subscriptions/socketIo\n\t\twebsocketEndpoint = appConfig.globalGraphEndpoint ~ websocketEndpointAPIEndpoint;\n\t}\n\t\n\t// The destructor should only clean up resources owned directly by this instance\n\t~this() {\n\t\tif (response !is null) {\n\t\t\tobject.destroy(response); // calls class CurlResponse destructor\n\t\t\tresponse = null;\n\t\t}\n\t\t\n\t\tif (curlEngine !is null) {\n\t\t\tobject.destroy(curlEngine); // calls class CurlEngine destructor\n\t\t\tcurlEngine = null;\n\t\t}\n\t\t\n\t\tif (appConfig !is null) {\n\t\t\tappConfig = null;\n\t\t}\n\t}\n\n\t// Initialise the OneDrive API class\n\tbool initialise(bool keepAlive=true) {\n\t\t// Initialise the curl engine\n\t\tthis.keepAlive = keepAlive;\n\t\tif (curlEngine is null) {\n\t\t\tcurlEngine = getCurlInstance();\n\t\t\tcurlEngine.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);\n\t\t\n\t\t\t// WebSocket capability available in OS cURL version\n\t\t\tif (!appConfig.websocketSupportCheckDone) {\n\t\t\t\t// Check the underlying cURL capability to support websockets\n\t\t\t\tif (debugLogging) {addLogEntry(\"Checking cURL Websocket support ...\", [\"debug\"]);}\n\t\t\t\tbool websocketSupport = curlSupportsWebSockets();\n\t\t\t\tif (debugLogging) {addLogEntry(\"Checked cURL Websocket support = \" ~ to!string(websocketSupport), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Update appConfig flags\n\t\t\t\tappConfig.curlSupportsWebSockets = websocketSupport;\n\t\t\t\tappConfig.websocketSupportCheckDone = true;\n\t\t\t\t\n\t\t\t\t// Notify user if cURL version is too old to support websockets, but only if we are in --monitor mode, as this is where this is used\n\t\t\t\t// Are we doing a --monitor operation?\n\t\t\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t\t\tif (!websocketSupport) {\n\t\t\t\t\t\t// cURL/libcurl version is too old\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"WARNING: Your libcurl version is too old for WebSocket support. Please upgrade to libcurl 7.86.0 or newer.\");\n\t\t\t\t\t\taddLogEntry(\"         The near real-time processing of online changes cannot be enabled on your system.\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Authorised value to return\n\t\tbool authorised = false;\n\n\t\t// Did the user specify --dry-run\n\t\tdryRun = appConfig.getValueBool(\"dry_run\");\n\t\t\n\t\t// Set clientId to use the configured 'application_id'\n\t\tclientId = appConfig.getValueString(\"application_id\");\n\t\tif (clientId != appConfig.defaultApplicationId) {\n\t\t\t// a custom 'application_id' was set\n\t\t\tcompanyName = \"custom_application\";\n\t\t}\n\t\t\n\t\t// Do we have a custom Azure Tenant ID?\n\t\tif (!appConfig.getValueString(\"azure_tenant_id\").empty) {\n\t\t\t// Use the value entered by the user\n\t\t\ttenantId = appConfig.getValueString(\"azure_tenant_id\");\n\t\t} else {\n\t\t\t// set to common\n\t\t\ttenantId = \"common\";\n\t\t}\n\t\t\n\t\t// Did the user specify a 'drive_id' ?\n\t\tif (!appConfig.getValueString(\"drive_id\").empty) {\n\t\t\t// Update base URL's\n\t\t\tdriveUrl = driveByIdUrl ~ appConfig.getValueString(\"drive_id\");\n\t\t\titemByIdUrl = driveUrl ~ \"/items\";\n\t\t\titemByPathUrl = driveUrl ~ \"/root:/\";\n\t\t\t\n\t\t\t// Need to update 'websocketEndpointAPIEndpoint' to /v1.0/drives/{driveId}/root/subscriptions/socketIo\n\t\t\twebsocketEndpointAPIEndpoint = \"/v1.0/drives/\" ~ appConfig.getValueString(\"drive_id\") ~ \"/root/subscriptions/socketIo\";\n\t\t}\n\t\t\n\t\t// Configure the authentication scope\n\t\tif (appConfig.getValueBool(\"read_only_auth_scope\")) {\n\t\t\t// read-only authentication scopes has been requested\n\t\t\tif (appConfig.getValueBool(\"use_device_auth\")) {\n\t\t\t\tauthScope = \"&scope=Files.Read%20Files.Read.All%20Sites.Read.All%20offline_access\";\n\t\t\t} else {\n\t\t\t\tauthScope = \"&scope=Files.Read%20Files.Read.All%20Sites.Read.All%20offline_access&response_type=code&prompt=login&redirect_uri=\";\n\t\t\t}\n\t\t} else {\n\t\t\t// read-write authentication scopes will be used (default)\n\t\t\tif (appConfig.getValueBool(\"use_device_auth\")) {\n\t\t\t\tauthScope = \"&scope=Files.ReadWrite%20Files.ReadWrite.All%20Sites.ReadWrite.All%20offline_access\";\n\t\t\t} else {\n\t\t\t\tauthScope = \"&scope=Files.ReadWrite%20Files.ReadWrite.All%20Sites.ReadWrite.All%20offline_access&response_type=code&prompt=login&redirect_uri=\";\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Configure Azure AD endpoints if 'azure_ad_endpoint' is configured\n\t\tstring azureConfigValue = appConfig.getValueString(\"azure_ad_endpoint\");\n\t\tswitch(azureConfigValue) {\n\t\t\tcase \"\":\n\t\t\t\tif (tenantId == \"common\") {\n\t\t\t\t\tif (!appConfig.apiWasInitialised) addLogEntry(\"Configuring Global Azure AD Endpoints\");\n\t\t\t\t} else {\n\t\t\t\t\tif (!appConfig.apiWasInitialised) addLogEntry(\"Configuring Global Azure AD Endpoints - Single Tenant Application\");\n\t\t\t\t}\n\t\t\t\t// Authentication\n\t\t\t\tauthUrl = appConfig.globalAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/authorize\";\n\t\t\t\tdeviceAuthUrl = appConfig.globalAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/devicecode\";\n\t\t\t\tredirectUrl = appConfig.globalAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\ttokenUrl = appConfig.globalAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/token\";\n\t\t\t\t// WebSocket Endpoint\n\t\t\t\twebsocketEndpoint = appConfig.globalGraphEndpoint ~ websocketEndpointAPIEndpoint;\n\t\t\t\tbreak;\n\t\t\tcase \"USL4\":\n\t\t\t\tif (!appConfig.apiWasInitialised) addLogEntry(\"Configuring Azure AD for US Government Endpoints\");\n\t\t\t\t// Authentication\n\t\t\t\tauthUrl = appConfig.usl4AuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/authorize\";\n\t\t\t\tdeviceAuthUrl = appConfig.usl4AuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/devicecode\";\n\t\t\t\ttokenUrl = appConfig.usl4AuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/token\";\n\t\t\t\tif (clientId == appConfig.defaultApplicationId) {\n\t\t\t\t\t// application_id == default\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"USL4 AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint\", [\"debug\"]);}\n\t\t\t\t\tredirectUrl = appConfig.globalAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\t} else {\n\t\t\t\t\t// custom application_id\n\t\t\t\t\tredirectUrl = appConfig.usl4AuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\t}\n\n\t\t\t\t// Drive Queries\n\t\t\t\tdriveUrl = appConfig.usl4GraphEndpoint ~ defaultDriveUrlAPIEndpoint;\n\t\t\t\tdriveByIdUrl = appConfig.usl4GraphEndpoint ~ defaultDriveByIdUrlAPIEndpoint;\n\t\t\t\t// Item Queries\n\t\t\t\titemByIdUrl = appConfig.usl4GraphEndpoint ~ defaultItemByIdUrlAPIEndpoint;\n\t\t\t\titemByPathUrl = appConfig.usl4GraphEndpoint ~ defaultItemByPathUrlAPIEndpoint;\n\t\t\t\t// Office 365 / SharePoint Queries\n\t\t\t\tsiteSearchUrl = appConfig.usl4GraphEndpoint ~ defaultSiteSearchUrlAPIEndpoint;\n\t\t\t\tsiteDriveUrl = appConfig.usl4GraphEndpoint ~ defaultSiteDriveUrlAPIEndpoint;\n\t\t\t\t// Shared With Me\n\t\t\t\tsharedWithMeUrl = appConfig.usl4GraphEndpoint ~ defaultSharedWithMeUrlAPIEndpoint;\n\t\t\t\t// Subscriptions\n\t\t\t\tsubscriptionUrl = appConfig.usl4GraphEndpoint ~ defaultSubscriptionUrlAPIEndpoint;\n\t\t\t\t// WebSocket Endpoint\n\t\t\t\twebsocketEndpoint = appConfig.usl4GraphEndpoint ~ websocketEndpointAPIEndpoint;\n\t\t\t\tbreak;\n\t\t\tcase \"USL5\":\n\t\t\t\tif (!appConfig.apiWasInitialised) addLogEntry(\"Configuring Azure AD for US Government Endpoints (DOD)\");\n\t\t\t\t// Authentication\n\t\t\t\tauthUrl = appConfig.usl5AuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/authorize\";\n\t\t\t\tdeviceAuthUrl = appConfig.usl5AuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/devicecode\";\n\t\t\t\ttokenUrl = appConfig.usl5AuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/token\";\n\t\t\t\tif (clientId == appConfig.defaultApplicationId) {\n\t\t\t\t\t// application_id == default\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"USL5 AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint\", [\"debug\"]);}\n\t\t\t\t\tredirectUrl = appConfig.globalAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\t} else {\n\t\t\t\t\t// custom application_id\n\t\t\t\t\tredirectUrl = appConfig.usl5AuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\t}\n\n\t\t\t\t// Drive Queries\n\t\t\t\tdriveUrl = appConfig.usl5GraphEndpoint ~ defaultDriveUrlAPIEndpoint;\n\t\t\t\tdriveByIdUrl = appConfig.usl5GraphEndpoint ~ defaultDriveByIdUrlAPIEndpoint;\n\t\t\t\t// Item Queries\n\t\t\t\titemByIdUrl = appConfig.usl5GraphEndpoint ~ defaultItemByIdUrlAPIEndpoint;\n\t\t\t\titemByPathUrl = appConfig.usl5GraphEndpoint ~ defaultItemByPathUrlAPIEndpoint;\n\t\t\t\t// Office 365 / SharePoint Queries\n\t\t\t\tsiteSearchUrl = appConfig.usl5GraphEndpoint ~ defaultSiteSearchUrlAPIEndpoint;\n\t\t\t\tsiteDriveUrl = appConfig.usl5GraphEndpoint ~ defaultSiteDriveUrlAPIEndpoint;\n\t\t\t\t// Shared With Me\n\t\t\t\tsharedWithMeUrl = appConfig.usl5GraphEndpoint ~ defaultSharedWithMeUrlAPIEndpoint;\n\t\t\t\t// Subscriptions\n\t\t\t\tsubscriptionUrl = appConfig.usl5GraphEndpoint ~ defaultSubscriptionUrlAPIEndpoint;\n\t\t\t\t// WebSocket Endpoint\n\t\t\t\twebsocketEndpoint = appConfig.usl5GraphEndpoint ~ websocketEndpointAPIEndpoint;\n\t\t\t\tbreak;\n\t\t\tcase \"DE\":\n\t\t\t\tif (!appConfig.apiWasInitialised) addLogEntry(\"Configuring Azure AD Germany\");\n\t\t\t\t// Authentication\n\t\t\t\tauthUrl = appConfig.deAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/authorize\";\n\t\t\t\tdeviceAuthUrl = appConfig.deAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/devicecode\";\n\t\t\t\ttokenUrl = appConfig.deAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/token\";\n\t\t\t\tif (clientId == appConfig.defaultApplicationId) {\n\t\t\t\t\t// application_id == default\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"DE AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint\", [\"debug\"]);}\n\t\t\t\t\tredirectUrl = appConfig.globalAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\t} else {\n\t\t\t\t\t// custom application_id\n\t\t\t\t\tredirectUrl = appConfig.deAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\t}\n\n\t\t\t\t// Drive Queries\n\t\t\t\tdriveUrl = appConfig.deGraphEndpoint ~ defaultDriveUrlAPIEndpoint;\n\t\t\t\tdriveByIdUrl = appConfig.deGraphEndpoint ~ defaultDriveByIdUrlAPIEndpoint;\n\t\t\t\t// Item Queries\n\t\t\t\titemByIdUrl = appConfig.deGraphEndpoint ~ defaultItemByIdUrlAPIEndpoint;\n\t\t\t\titemByPathUrl = appConfig.deGraphEndpoint ~ defaultItemByPathUrlAPIEndpoint;\n\t\t\t\t// Office 365 / SharePoint Queries\n\t\t\t\tsiteSearchUrl = appConfig.deGraphEndpoint ~ defaultSiteSearchUrlAPIEndpoint;\n\t\t\t\tsiteDriveUrl = appConfig.deGraphEndpoint ~ defaultSiteDriveUrlAPIEndpoint;\n\t\t\t\t// Shared With Me\n\t\t\t\tsharedWithMeUrl = appConfig.deGraphEndpoint ~ defaultSharedWithMeUrlAPIEndpoint;\n\t\t\t\t// Subscriptions\n\t\t\t\tsubscriptionUrl = appConfig.deGraphEndpoint ~ defaultSubscriptionUrlAPIEndpoint;\n\t\t\t\t// WebSocket Endpoint\n\t\t\t\twebsocketEndpoint = appConfig.deGraphEndpoint ~ websocketEndpointAPIEndpoint;\n\t\t\t\tbreak;\n\t\t\tcase \"CN\":\n\t\t\t\tif (!appConfig.apiWasInitialised) addLogEntry(\"Configuring AD China operated by VNET\");\n\t\t\t\t// Authentication\n\t\t\t\tauthUrl = appConfig.cnAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/authorize\";\n\t\t\t\tdeviceAuthUrl = appConfig.cnAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/devicecode\";\n\t\t\t\ttokenUrl = appConfig.cnAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/v2.0/token\";\n\t\t\t\tif (clientId == appConfig.defaultApplicationId) {\n\t\t\t\t\t// application_id == default\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"CN AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint\", [\"debug\"]);}\n\t\t\t\t\tredirectUrl = appConfig.globalAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\t} else {\n\t\t\t\t\t// custom application_id\n\t\t\t\t\tredirectUrl = appConfig.cnAuthEndpoint ~ \"/\" ~ tenantId ~ \"/oauth2/nativeclient\";\n\t\t\t\t}\n\n\t\t\t\t// Drive Queries\n\t\t\t\tdriveUrl = appConfig.cnGraphEndpoint ~ defaultDriveUrlAPIEndpoint;\n\t\t\t\tdriveByIdUrl = appConfig.cnGraphEndpoint ~ defaultDriveByIdUrlAPIEndpoint;\n\t\t\t\t// Item Queries\n\t\t\t\titemByIdUrl = appConfig.cnGraphEndpoint ~ defaultItemByIdUrlAPIEndpoint;\n\t\t\t\titemByPathUrl = appConfig.cnGraphEndpoint ~ defaultItemByPathUrlAPIEndpoint;\n\t\t\t\t// Office 365 / SharePoint Queries\n\t\t\t\tsiteSearchUrl = appConfig.cnGraphEndpoint ~ defaultSiteSearchUrlAPIEndpoint;\n\t\t\t\tsiteDriveUrl = appConfig.cnGraphEndpoint ~ defaultSiteDriveUrlAPIEndpoint;\n\t\t\t\t// Shared With Me\n\t\t\t\tsharedWithMeUrl = appConfig.cnGraphEndpoint ~ defaultSharedWithMeUrlAPIEndpoint;\n\t\t\t\t// Subscriptions\n\t\t\t\tsubscriptionUrl = appConfig.cnGraphEndpoint ~ defaultSubscriptionUrlAPIEndpoint;\n\t\t\t\t// WebSocket Endpoint\n\t\t\t\twebsocketEndpoint = appConfig.cnGraphEndpoint ~ websocketEndpointAPIEndpoint;\n\t\t\t\tbreak;\n\t\t\t// Default - all other entries\n\t\t\tdefault:\n\t\t\t\tif (!appConfig.apiWasInitialised) addLogEntry(\"Unknown Azure AD Endpoint request - using Global Azure AD Endpoints\");\n\t\t}\n\t\t\n\t\t// Has the application been authenticated?\n\t\t// How do we authenticate - standard method or via Intune?\n\t\tif (appConfig.getValueBool(\"use_intune_sso\")) {\n\t\t\t// Authenticate via Intune\n\t\t\tif (appConfig.accessToken.empty) {\n\t\t\t\t// No authentication via intune yet\n\t\t\t\tauthorised = authorise();\n\t\t\t} else {\n\t\t\t\t// We are already authenticated\n\t\t\t\tauthorised = true;\n\t\t\t}\n\t\t} else {\n\t\t\t// Authenticate via standard method\n\t\t\tif (!exists(appConfig.refreshTokenFilePath)) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Application has no 'refresh_token' thus needs to be authenticated\", [\"debug\"]);}\n\t\t\t\tauthorised = authorise();\n\t\t\t} else {\n\t\t\t\t// Try and read the value from the appConfig if it is set, rather than trying to read the value from disk\n\t\t\t\tif (!appConfig.refreshToken.empty) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Read token from appConfig\", [\"debug\"]);}\n\t\t\t\t\trefreshToken = strip(appConfig.refreshToken);\n\t\t\t\t\tauthorised = true;\n\t\t\t\t} else {\n\t\t\t\t\t// Try and read the file from disk\n\t\t\t\t\ttry {\n\t\t\t\t\t\trefreshToken = strip(readText(appConfig.refreshTokenFilePath));\n\t\t\t\t\t\t// is the refresh_token empty?\n\t\t\t\t\t\tif (refreshToken.empty) {\n\t\t\t\t\t\t\taddLogEntry(\"RefreshToken exists but is empty: \" ~ appConfig.refreshTokenFilePath);\n\t\t\t\t\t\t\tauthorised = authorise();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Existing token not empty\n\t\t\t\t\t\t\tauthorised = true;\n\t\t\t\t\t\t\t// update appConfig.refreshToken\n\t\t\t\t\t\t\tappConfig.refreshToken = refreshToken;\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (FileException exception) {\n\t\t\t\t\t\tauthorised = authorise();\n\t\t\t\t\t} catch (std.utf.UTFException exception) {\n\t\t\t\t\t\t// path contains characters which generate a UTF exception\n\t\t\t\t\t\taddLogEntry(\"Cannot read refreshToken from: \" ~ appConfig.refreshTokenFilePath);\n\t\t\t\t\t\taddLogEntry(\"  Error Reason:\" ~ exception.msg);\n\t\t\t\t\t\tauthorised = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (refreshToken.empty) {\n\t\t\t\t\t// PROBLEM ... CODING TO DO ??????????\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"DEBUG: refreshToken is empty !!!!!!!!!!\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Return if we are authorised\n\t\tif (debugLogging) {addLogEntry(\"Authorised State: \" ~ to!string(authorised), [\"debug\"]);}\n\t\treturn authorised;\n\t}\n\n\t// If the API has been configured correctly, print the items that been configured\n\tvoid debugOutputConfiguredAPIItems() {\n\t\t// Debug output of configured URL's\n\t\t// Application Identification\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Configured clientId           \" ~ clientId, [\"debug\"]);\n\t\t\taddLogEntry(\"Configured userAgent          \" ~ appConfig.getValueString(\"user_agent\"), [\"debug\"]);\n\t\t\t// Authentication\n\t\t\taddLogEntry(\"Configured authScope:         \" ~ authScope, [\"debug\"]);\n\t\t\taddLogEntry(\"Configured authUrl:           \" ~ authUrl, [\"debug\"]);\n\t\t\taddLogEntry(\"Configured redirectUrl:       \" ~ redirectUrl, [\"debug\"]);\n\t\t\taddLogEntry(\"Configured tokenUrl:          \" ~ tokenUrl, [\"debug\"]);\n\t\t\t// Drive Queries\n\t\t\taddLogEntry(\"Configured driveUrl:          \" ~ driveUrl, [\"debug\"]);\n\t\t\taddLogEntry(\"Configured driveByIdUrl:      \" ~ driveByIdUrl, [\"debug\"]);\n\t\t\t// Shared With Me\n\t\t\taddLogEntry(\"Configured sharedWithMeUrl:   \" ~ sharedWithMeUrl, [\"debug\"]);\n\t\t\t// Item Queries\n\t\t\taddLogEntry(\"Configured itemByIdUrl:       \" ~ itemByIdUrl, [\"debug\"]);\n\t\t\taddLogEntry(\"Configured itemByPathUrl:     \" ~ itemByPathUrl, [\"debug\"]);\n\t\t\t// SharePoint Queries\n\t\t\taddLogEntry(\"Configured siteSearchUrl:     \" ~ siteSearchUrl, [\"debug\"]);\n\t\t\taddLogEntry(\"Configured siteDriveUrl:      \" ~ siteDriveUrl, [\"debug\"]);\n\t\t\t// Websocket \n\t\t\taddLogEntry(\"Configured websocketEndpoint: \" ~ websocketEndpoint, [\"debug\"]);\n\t\t}\n\t}\n\t\n\t// Release CurlEngine bask to the Curl Engine Pool\n\tvoid releaseCurlEngine() {\n\t\t// Log that this was called\n\t\tif ((debugLogging) && (debugHTTPSResponse)) {addLogEntry(\"OneDrive API releaseCurlEngine() Called\", [\"debug\"]);}\n\t\t\n\t\t// Release curl instance back to the pool\n\t\tif (curlEngine !is null) {\n\t\t\tcurlEngine.releaseEngine();\n\t\t\tcurlEngine = null;\n\t\t}\n\t\t// Release the response\n\t\tresponse = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t}\n\n\t// Authenticate this client against Microsoft OneDrive API using one of the 3 authentication methods this client supports\n\tbool authorise() {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\t// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session\n\t\tif (appConfig.getValueBool(\"use_intune_sso\")) {\n\t\t\t// The client is configured to use Intune SSO via Microsoft Identity Broker dbus session\n\t\t\t// Do we have a saved account file?\n\t\t\tif (!exists(appConfig.intuneAccountDetailsFilePath)) {\n\t\t\t\t// No file exists locally\n\t\t\t\tauto intuneAuthResult = acquire_token_interactive(appConfig.getValueString(\"application_id\"));\n\t\t\t\tJSONValue intuneBrokerJSONData = intuneAuthResult.brokerTokenResponse;\n\t\t\t\t\n\t\t\t\t// Is the response JSON data valid?\n\t\t\t\tif ((intuneBrokerJSONData.type() == JSONType.object)) {\n\t\t\t\t\t// Does the JSON data have the required authentication elements:\n\t\t\t\t\t// - accessToken\n\t\t\t\t\t// - account\n\t\t\t\t\t// - expiresOn\n\t\t\t\t\tif ((hasAccessTokenData(intuneBrokerJSONData)) && (hasAccountData(intuneBrokerJSONData)) && (hasExpiresOn(intuneBrokerJSONData))) {\n\t\t\t\t\t\t// Details exist\n\t\t\t\t\t\tprocessIntuneResponse(intuneBrokerJSONData);\n\t\t\t\t\t\t// Return that we are authenticated\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// no ... expected values not available\n\t\t\t\t\t\taddLogEntry(\"Required JSON elements are not present in the Intune JSON response\");\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Not a valid JSON response\n\t\t\t\t\taddLogEntry(\"Invalid JSON Intune JSON response when attempting access authentication\");\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// The account information is available in a saved file. Read this file in and attempt a silent authentication\n\t\t\t\ttry {\n\t\t\t\t\tappConfig.intuneAccountDetails = strip(readText(appConfig.intuneAccountDetailsFilePath));\n\t\t\t\t\t\n\t\t\t\t\t// Is the 'intune_account' empty?\n\t\t\t\t\tif (appConfig.intuneAccountDetails.empty) {\n\t\t\t\t\t\taddLogEntry(\"The 'intune_account' file exists but is empty: \" ~ appConfig.intuneAccountDetailsFilePath);\n\t\t\t\t\t\t// No .. remove the file and perform the interactive authentication\n\t\t\t\t\t\tsafeRemove(appConfig.intuneAccountDetailsFilePath);\n\t\t\t\t\t\t// Attempt interactive authentication\n\t\t\t\t\t\tauthorise();\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// We have loaded some Intune Account details, try and use them\n\t\t\t\t\t\tauto intuneAuthResult = acquire_token_silently(appConfig.intuneAccountDetails, appConfig.getValueString(\"application_id\"));\n\t\t\t\t\t\tJSONValue intuneBrokerJSONData = intuneAuthResult.brokerTokenResponse;\n\t\t\t\t\t\t// Is the JSON data valid?\n\t\t\t\t\t\tif ((intuneBrokerJSONData.type() == JSONType.object)) {\n\t\t\t\t\t\t\t// Does the JSON data have the required authentication elements:\n\t\t\t\t\t\t\t// - accessToken\n\t\t\t\t\t\t\t// - account\n\t\t\t\t\t\t\t// - expiresOn\n\t\t\t\t\t\t\tif ((hasAccessTokenData(intuneBrokerJSONData)) && (hasAccountData(intuneBrokerJSONData)) && (hasExpiresOn(intuneBrokerJSONData))) {\n\t\t\t\t\t\t\t\t// Details exist\n\t\t\t\t\t\t\t\tprocessIntuneResponse(intuneBrokerJSONData);\n\t\t\t\t\t\t\t\t// Return that we are authenticated\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// no ... expected values not available\n\t\t\t\t\t\t\t\taddLogEntry(\"Required JSON elements are not present in the Intune JSON response\");\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// No .. remove the file and perform the interactive authentication\n\t\t\t\t\t\t\tsafeRemove(appConfig.intuneAccountDetailsFilePath);\n\t\t\t\t\t\t\t// Attempt interactive authentication\n\t\t\t\t\t\t\tauthorise();\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (FileException exception) {\n\t\t\t\t\treturn false;\n\t\t\t\t} catch (std.utf.UTFException exception) {\n\t\t\t\t\t// path contains characters which generate a UTF exception\n\t\t\t\t\taddLogEntry(\"Cannot read 'intune_account' file on disk from: \" ~ appConfig.intuneAccountDetailsFilePath);\n\t\t\t\t\taddLogEntry(\"  Error Reason:\" ~ exception.msg);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// There are 2 options here for normal authentication flow\n\t\t\t// 1. Use OAuth2 Device Authorisation Flow\n\t\t\t// 2. Use OAuth2 Interactive Authorisation Flow (application default)\n\t\t\tstring authoriseApplicationRequest = \"Please authorise this application by visiting the following URL:\\n\";\n\t\t\t\n\t\t\tif (appConfig.getValueBool(\"use_device_auth\")) {\n\t\t\t\t// Use OAuth2 Device Authorisation Flow\n\t\t\t\t// * deviceAuthUrl: Should already be configured based on client configuration\n\t\t\t\t// * tokenUrl: Should already be configured based on client configuration\n\t\t\t\t// * authScope: Should already be configured with the correct auth scopes\n\t\t\t\tstring deviceAuthPostData = \"client_id=\" ~ clientId ~ authScope;\n\t\t\t\t\n\t\t\t\t// Initiating Device Code Request\n\t\t\t\tJSONValue deviceAuthResponse = initiateDeviceAuthorisation(deviceAuthPostData);\n\t\t\t\t\n\t\t\t\t// Was a valid JSON response provided?\n\t\t\t\tif (deviceAuthResponse.type() == JSONType.object) {\n\t\t\t\t\t// A valid JSON was returned\n\t\t\t\t\t// Extract required values\n\t\t\t\t\tstring deviceCode = deviceAuthResponse[\"device_code\"].str;\n\t\t\t\t\tstring deviceAuthUrl = deviceAuthResponse[\"verification_uri\"].str;\n\t\t\t\t\tstring userCode = deviceAuthResponse[\"user_code\"].str;\n\t\t\t\t\tlong expiresIn = deviceAuthResponse[\"expires_in\"].integer;\n\t\t\t\t\tlong pollInterval = deviceAuthResponse[\"interval\"].integer;\n\t\t\t\t\tSysTime expiresAt = Clock.currTime + dur!\"seconds\"(expiresIn);\n\t\t\t\t\texpiresAt.fracSecs = Duration.zero;\n\t\t\t\t\t\n\t\t\t\t\t// Display the required items for the user to action\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\taddLogEntry(authoriseApplicationRequest, [\"consoleOnly\"]);\n\t\t\t\t\taddLogEntry(deviceAuthUrl ~ \"\\n\", [\"consoleOnly\"]);\n\t\t\t\t\taddLogEntry(\"Enter the following code when prompted: \" ~ userCode, [\"consoleOnly\"]);\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\taddLogEntry(\"This code expires at: \" ~ to!string(expiresAt), [\"consoleOnly\"]);\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\n\t\t\t\t\t// JSON value to store the poll response data\n\t\t\t\t\tJSONValue deviceAuthPollResponse;\n\t\t\t\t\t\n\t\t\t\t\t// Construct the polling post submission data\n\t\t\t\t\tstring pollPostData = format(\n\t\t\t\t\t\t\"client_id=%s&grant_type=urn%%3Aietf%%3Aparams%%3Aoauth%%3Agrant-type%%3Adevice_code&device_code=%s\",\n\t\t\t\t\t\tclientId,\n\t\t\t\t\t\tdeviceCode\n\t\t\t\t\t);\n\t\t\t\t\t\n\t\t\t\t\t// Poll Microsoft API for authentication to be performed, until the expiry of this device authentication request\n\t\t\t\t\twhile (Clock.currTime < expiresAt) {\n\t\t\t\t\t\t// Try the post to poll if the authentication has been done\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdeviceAuthPollResponse = post(tokenUrl, pollPostData, null, true, \"application/x-www-form-urlencoded\");\n\t\t\t\t\t\t\t// No error ... break out of the loop so the returned data can be processed\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t} catch (OneDriveException e) {\n\t\t\t\t\t\t\t// Get the polling error JSON response\n\t\t\t\t\t\t\tJSONValue errorResponse = e.error;\n\t\t\t\t\t\t\tstring errorType;\n\n\t\t\t\t\t\t\tif (\"error\" in errorResponse) {\n\t\t\t\t\t\t\t\terrorType = errorResponse[\"error\"].str;\n\n\t\t\t\t\t\t\t\tif (errorType == \"authorization_pending\") {\n\t\t\t\t\t\t\t\t\t// Calculate remaining time\n\t\t\t\t\t\t\t\t\tDuration timeRemaining = expiresAt - Clock.currTime;\n\t\t\t\t\t\t\t\t\tlong minutes = timeRemaining.total!\"minutes\"();\n\t\t\t\t\t\t\t\t\tlong seconds = timeRemaining.total!\"seconds\"() % 60;\n\n\t\t\t\t\t\t\t\t\t// Log countdown and status\n\t\t\t\t\t\t\t\t\taddLogEntry(format(\"[%02dm %02ds remaining] Still pending authorisation ...\", minutes, seconds));\n\t\t\t\t\t\t\t\t} else if (errorType == \"authorization_declined\") {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Authorisation was declined by the user.\");\n\t\t\t\t\t\t\t\t\t// return false if we get to this point\n\t\t\t\t\t\t\t\t\t// set 'use_device_auth' to false to fall back to interactive authentication flow\n\t\t\t\t\t\t\t\t\tappConfig.setValueBool(\"use_device_auth\" , false);\n\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t} else if (errorType == \"expired_token\") {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Device code expired before authorisation was completed.\");\n\t\t\t\t\t\t\t\t\t// return false if we get to this point\n\t\t\t\t\t\t\t\t\t// set 'use_device_auth' to false to fall back to interactive authentication flow\n\t\t\t\t\t\t\t\t\tappConfig.setValueBool(\"use_device_auth\" , false);\n\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Unhandled error during polling: \" ~ errorType);\n\t\t\t\t\t\t\t\t\t// return false if we get to this point\n\t\t\t\t\t\t\t\t\t// set 'use_device_auth' to false to fall back to interactive authentication flow\n\t\t\t\t\t\t\t\t\tappConfig.setValueBool(\"use_device_auth\" , false);\n\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\taddLogEntry(\"Unexpected error response from token polling.\");\n\t\t\t\t\t\t\t\t// return false if we get to this point\n\t\t\t\t\t\t\t\t// set 'use_device_auth' to false to fall back to interactive authentication flow\n\t\t\t\t\t\t\t\tappConfig.setValueBool(\"use_device_auth\" , false);\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sleep until next polling interval\n\t\t\t\t\t\tThread.sleep(dur!\"seconds\"(pollInterval));\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Broken out of the polling loop\n\t\t\t\t\t// Was a valid JSON response provided?\n\t\t\t\t\tif (deviceAuthPollResponse.type() == JSONType.object) {\n\t\t\t\t\t\t// is the required 'access_token' available?\n\t\t\t\t\t\tif (\"access_token\" in deviceAuthPollResponse) {\n\t\t\t\t\t\t\t// We got the applicable access token\n\t\t\t\t\t\t\taddLogEntry(\"Access token acquired!\");\n\t\t\t\t\t\t\t// Process this JSON data\n\t\t\t\t\t\t\tprocessAuthenticationJSON(deviceAuthPollResponse);\n\t\t\t\t\t\t\t// Return that we are authorised\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// return false if we get to this point\n\t\t\t\t\t// set 'use_device_auth' to false to fall back to interactive authentication flow\n\t\t\t\t\tappConfig.setValueBool(\"use_device_auth\" , false);\n\t\t\t\t\treturn false;\n\t\t\t\t} else {\n\t\t\t\t\t// No valid JSON response was returned\n\t\t\t\t\t// set 'use_device_auth' to false to fall back to interactive authentication flow\n\t\t\t\t\tappConfig.setValueBool(\"use_device_auth\" , false);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Use OAuth2 Interactive Authorisation Flow (application default)\n\t\t\t\tchar[] response;\n\t\t\t\t// What URL should be presented to the user to access\n\t\t\t\tstring url = authUrl ~ \"?client_id=\" ~ clientId ~ authScope ~ redirectUrl;\n\t\t\t\t// Configure automated authentication if --auth-files authUrlFilePath:responseUrlFilePath is being used\n\t\t\t\tstring authFilesString = appConfig.getValueString(\"auth_files\");\n\t\t\t\tstring authResponseString = appConfig.getValueString(\"auth_response\");\n\t\t\t\n\t\t\t\tif (!authResponseString.empty) {\n\t\t\t\t\t// read the response from authResponseString\n\t\t\t\t\tresponse = cast(char[]) authResponseString;\n\t\t\t\t} else if (authFilesString != \"\") {\n\t\t\t\t\tstring[] authFiles = authFilesString.split(\":\");\n\t\t\t\t\tstring authUrlFilePath = authFiles[0];\n\t\t\t\t\tstring responseUrlFilePath = authFiles[1];\n\t\t\t\t\t\n\t\t\t\t\ttry {\n\t\t\t\t\t\tauto authUrlFile = File(authUrlFilePath, \"w\");\n\t\t\t\t\t\tauthUrlFile.write(url);\n\t\t\t\t\t\tauthUrlFile.close();\n\t\t\t\t\t} catch (FileException exception) {\n\t\t\t\t\t\t// There was a file system error\n\t\t\t\t\t\t// display the error message\n\t\t\t\t\t\tdisplayFileSystemErrorMessage(exception.msg, thisFunctionName, authUrlFilePath);\n\t\t\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\t\t\tforceExit();\n\t\t\t\t\t} catch (ErrnoException exception) {\n\t\t\t\t\t\t// There was a file system error\n\t\t\t\t\t\t// display the error message\n\t\t\t\t\t\tdisplayFileSystemErrorMessage(exception.msg, thisFunctionName, authUrlFilePath);\n\t\t\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\t\t\tforceExit();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Log we are now waiting\n\t\t\t\t\taddLogEntry(\"Client requires authentication before proceeding. Waiting for --auth-files elements to be available.\");\n\t\t\t\t\t\n\t\t\t\t\twhile (!exists(responseUrlFilePath)) {\n\t\t\t\t\t\tThread.sleep(dur!(\"msecs\")(100));\n\t\t\t\t\t}\n\n\t\t\t\t\t// read response from provided from OneDrive\n\t\t\t\t\ttry {\n\t\t\t\t\t\tresponse = cast(char[]) read(responseUrlFilePath);\n\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\t// exception generated\n\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\t// try to remove auth files one at a time\n\t\t\t\t\ttry {\n\t\t\t\t\t\tstd.file.remove(authUrlFilePath);\n\t\t\t\t\t\t\n\t\t\t\t\t} catch (FileException exception) {\n\t\t\t\t\t\taddLogEntry(\"Cannot remove --auth-files elements - details below\");\n\t\t\t\t\t\t// There was a file system error - display the error message\n\t\t\t\t\t\tdisplayFileSystemErrorMessage(exception.msg, thisFunctionName, authUrlFilePath);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\ttry {\n\t\t\t\t\t\tstd.file.remove(responseUrlFilePath);\n\t\t\t\t\t} catch (FileException exception) {\n\t\t\t\t\t\taddLogEntry(\"Cannot remove --auth-files elements - details below\");\n\t\t\t\t\t\t// There was a file system error - display the error message\n\t\t\t\t\t\tdisplayFileSystemErrorMessage(exception.msg, thisFunctionName, responseUrlFilePath);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// If we are not running in --dry-run mode, prompt the user to authorise the application\n\t\t\t\t\tif (!appConfig.getValueBool(\"dry_run\")) {\n\t\t\t\t\t\t// Notify the user of the next step: visit the URL to authorise the client\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(authoriseApplicationRequest, [\"consoleOnly\"]);\n\t\t\t\t\t\taddLogEntry(url ~ \"\\n\", [\"consoleOnly\"]);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Prompt the user to paste the full redirect URI (copied from the browser after login)\n\t\t\t\t\t\taddLogEntry(\"After completing the authorisation in your browser, copy the full redirect URI (from the address bar) and paste it below.\\n\", [\"consoleOnly\"]);\n\t\t\t\t\t\taddLogEntry(\"Paste redirect URI here: \", [\"consoleOnlyNoNewLine\"]);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Read the user's pasted response URI\n\t\t\t\t\t\treadln(response);\n\t\t\t\t\t\t// Flag that a response URI has been received - at this point could be valid or invalid\n\t\t\t\t\t\tappConfig.applicationAuthoriseResponseURIReceived = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"The application requires authorisation, which involves saving authentication data on your system. Application authorisation cannot be completed when using the '--dry-run' option.\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"To authorise the application please use your original command without '--dry-run'.\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"To exclusively authorise the application without performing any additional actions, do not add '--sync' or '--monitor' to your command line.\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\tforceExit();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// match the authorisation code\n\t\t\t\tauto c = matchFirst(strip(response), r\"(?:[?&]code=)([^&]+)\");\n\t\t\t\t\n\t\t\t\tif (c.empty) {\n\t\t\t\t\taddLogEntry(\"An empty or invalid response uri was entered\");\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tc.popFront(); // skip the whole match\n\t\t\t\tstring authCode = decodeComponent(c.front);\n\t\t\t\tredeemToken(authCode);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Process Intune JSON response data\n\tvoid processIntuneResponse(JSONValue intuneBrokerJSONData) {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t\n\t\t// Use the provided JSON data and configure elements, save JSON data to disk for reuse\n\t\tlong expiresOnMs = intuneBrokerJSONData[\"expiresOn\"].integer();\n\t\t// Convert to SysTime \n\t\tSysTime expiryTime = SysTime.fromUnixTime(expiresOnMs / 1000);\n\n\t\t// Store in appConfig (to match standard flow)\n\t\tappConfig.accessTokenExpiration = expiryTime;\n\t\taddLogEntry(\"Intune access token expires at: \" ~ to!string(appConfig.accessTokenExpiration));\n\t\t\n\t\t// Configure the 'accessToken' based on Intune response\n\t\tappConfig.accessToken = \"bearer \" ~ strip(intuneBrokerJSONData[\"accessToken\"].str);\n\t\t\n\t\t// Do we print the current access token\n\t\tdebugOutputAccessToken();\n\t\t\n\t\t// In order to support silent renewal of the access token, when the access token expires, we must store the Intune account data\n\t\tappConfig.intuneAccountDetails = to!string(intuneBrokerJSONData[\"account\"]);\n\t\t\n\t\t// try and update the 'intune_account' file on disk for reuse later\n\t\ttry {\n\t\t\tif (debugLogging) {addLogEntry(\"Updating 'intune_account' on disk\", [\"debug\"]);}\n\t\t\tstd.file.write(appConfig.intuneAccountDetailsFilePath, appConfig.intuneAccountDetails);\n\t\t\tif (debugLogging) {addLogEntry(\"Setting file permissions for: \" ~ appConfig.intuneAccountDetailsFilePath, [\"debug\"]);}\n\t\t\tappConfig.intuneAccountDetailsFilePath.setAttributes(appConfig.returnSecureFilePermission());\n\t\t} catch (FileException exception) {\n\t\t\t// display the error message\n\t\t\tdisplayFileSystemErrorMessage(exception.msg, thisFunctionName, appConfig.intuneAccountDetailsFilePath);\n\t\t}\n\t}\n\t\n\t// Initiate OAuth2 Device Authorisation\n\tJSONValue initiateDeviceAuthorisation(string deviceAuthPostData) {\n\t\t// Device OAuth2 Device Authorisation requires a HTTP POST\n\t\treturn post(deviceAuthUrl, deviceAuthPostData, null, true, \"application/x-www-form-urlencoded\");\n\t}\n\t\n\t// Do we print the current access token\n\tvoid debugOutputAccessToken() {\n\t\tif (appConfig.verbosityCount > 1) {\n\t\t\tif (appConfig.getValueBool(\"debug_https\")) {\n\t\t\t\tif (appConfig.getValueBool(\"print_token\")) {\n\t\t\t\t\t// This needs to be highly restricted in output .... \n\t\t\t\t\tif (debugLogging) {addLogEntry(\"CAUTION - KEEP THIS SAFE: Current access token: \" ~ to!string(appConfig.accessToken), [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get\n\tJSONValue getDefaultDriveDetails() {\n\t\tstring url;\n\t\turl = driveUrl;\n\t\treturn get(url);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get\n\tJSONValue getDefaultRootDetails() {\n\t\tstring url;\n\t\turl = driveUrl ~ \"/root\";\n\t\treturn get(url);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get\n\tJSONValue getDriveIdRoot(string driveId) {\n\t\tstring url;\n\t\turl = driveByIdUrl ~ driveId ~ \"/root\";\n\t\treturn get(url);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get\n\tJSONValue getDriveQuota(string driveId) {\n\t\tstring url;\n\t\turl = driveByIdUrl ~ driveId ~ \"/\";\n\t\turl ~= \"?select=quota\";\n\t\treturn get(url);\n\t}\n\t\n\t// Return the details of the specified path, by giving the path we wish to query\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get\n\tJSONValue getPathDetails(string path) {\n\t\tstring url;\n\t\tif ((path == \".\")||(path == \"/\")) {\n\t\t\turl = driveUrl ~ \"/root/\";\n\t\t} else {\n\t\t\turl = itemByPathUrl ~ encodeComponent(path) ~ \":/\";\n\t\t}\n\t\t// Add select clause\n\t\turl ~= \"?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy,package\";\n\t\treturn get(url);\n\t}\n\t\n\t// Return the details of the specified item based on its driveID and itemID\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get\n\tJSONValue getPathDetailsById(string driveId, string id) {\n\t\tstring url;\n\t\turl = driveByIdUrl ~ driveId ~ \"/items/\" ~ id;\n\t\turl ~= \"?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy,webUrl,lastModifiedDateTime,package\";\n\t\treturn get(url);\n\t}\n\t\n\t// Return all the items that are shared with the user\n\t// https://docs.microsoft.com/en-us/graph/api/drive-sharedwithme\n\tJSONValue getSharedWithMe() {\n\t\treturn get(sharedWithMeUrl);\n\t}\n\t\n\t// Create a shareable link for an existing file on OneDrive based on the accessScope JSON permissions\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createlink\n\tJSONValue createShareableLink(string driveId, string id, JSONValue accessScope) {\n\t\tstring url;\n\t\turl = driveByIdUrl ~ driveId ~ \"/items/\" ~ id ~ \"/createLink\";\n\t\treturn post(url, accessScope.toString());\n\t}\n\t\n\t// Return the requested details of the specified path on the specified drive id and path\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get\n\tJSONValue getPathDetailsByDriveId(string driveId, string path) {\n\t\tstring url;\n\t\t// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/concepts/addressing-driveitems?view=odsp-graph-online\n\t\t// Required format: /drives/{drive-id}/root:/{item-path}:\n\t\turl = driveByIdUrl ~ driveId ~ \"/root:/\" ~ encodeComponent(path) ~ \":\";\n\t\turl ~= \"?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy,package\";\n\t\treturn get(url);\n\t}\n\t\n\t// Track changes for a given driveId\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta\n\t//   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. \n\t//   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.\n\t//   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.\n\tJSONValue getChangesByItemId(string driveId, string id, string deltaLink) {\n\t\tstring[string] requestHeaders;\n\t\t\n\t\t// From March 1st 2025, this needs to be added to ensure that Shared Folders are sent in the Delta Query Response\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// OneDrive Personal Account\n\t\t\taddIncludeFeatureRequestHeader(&requestHeaders);\n\t\t} else {\n\t\t\t// Business or SharePoint Library\n\t\t\t// Only add if configured to do so\n\t\t\tif (appConfig.getValueBool(\"sync_business_shared_items\")) {\n\t\t\t\t// Feature enabled, add headers\n\t\t\t\taddIncludeFeatureRequestHeader(&requestHeaders);\n\t\t\t}\n\t\t}\n\t\t\n\t\tstring url;\n\t\t// configure deltaLink to query\n\t\tif (deltaLink.empty) {\n\t\t\turl = driveByIdUrl ~ driveId ~ \"/items/\" ~ id ~ \"/delta\";\n\t\t\t// 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\n\t\t\turl ~= \"?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy,package\";\n\t\t} else {\n\t\t\turl = deltaLink;\n\t\t}\n\t\t\n\t\t// get the response\n\t\treturn get(url, false, requestHeaders);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children\n\tJSONValue listChildren(string driveId, string id, string nextLink) {\n\t\tstring[string] requestHeaders;\n\t\t\n\t\t// From March 1st 2025, this needs to be added to ensure that Shared Folders are sent in the Delta Query Response\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// OneDrive Personal Account\n\t\t\taddIncludeFeatureRequestHeader(&requestHeaders);\n\t\t} else {\n\t\t\t// Business or SharePoint Library\n\t\t\t// Only add if configured to do so\n\t\t\tif (appConfig.getValueBool(\"sync_business_shared_items\")) {\n\t\t\t\t// Feature enabled, add headers\n\t\t\t\taddIncludeFeatureRequestHeader(&requestHeaders);\n\t\t\t}\n\t\t}\n\t\t\n\t\tstring url;\n\t\t// configure URL to query\n\t\tif (nextLink.empty) {\n\t\t\turl = driveByIdUrl ~ driveId ~ \"/items/\" ~ id ~ \"/children\";\n\t\t\turl ~= \"?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy,package\";\n\t\t} else {\n\t\t\turl = nextLink;\n\t\t}\n\t\treturn get(url, false, requestHeaders);\n\t}\n\t\n\t// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search\n\tJSONValue searchDriveForPath(string driveId, string path) {\n\t\t// OData string literal escaping: a single quote inside a '...' literal becomes doubled.\n\t\t// Then URL-encode for safe transport\n\t\tauto odataSafe = path.replace(\"'\", \"''\");\n\t\tauto encoded   = encodeComponent(odataSafe);\n\t\tstring url;\n\t\turl = \"https://graph.microsoft.com/v1.0/drives/\" ~ driveId ~ \"/root/search(q='\" ~ encoded ~ \"')\";\n\t\treturn get(url);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update\n\tJSONValue updateById(const(char)[] driveId, const(char)[] id, JSONValue data, const(char)[] eTag = null) {\n\t\tstring[string] requestHeaders;\n\t\tconst(char)[] url = driveByIdUrl ~ driveId ~ \"/items/\" ~ id;\n\t\tif (eTag) requestHeaders[\"If-Match\"] = to!string(eTag);\n\t\treturn patch(url, data.toString(), false, requestHeaders);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete\n\tvoid deleteById(const(char)[] driveId, const(char)[] id, const(char)[] eTag = null) {\n\t\t// string[string] requestHeaders;\n\t\tconst(char)[] url = driveByIdUrl ~ driveId ~ \"/items/\" ~ id;\n\t\t//TODO: investigate why this always fail with 412 (Precondition Failed)\n\t\t// if (eTag) requestHeaders[\"If-Match\"] = eTag;\n\t\tperformDelete(url);\n\t}\n\t\n\t// https://learn.microsoft.com/en-us/graph/api/driveitem-permanentdelete?view=graph-rest-1.0\n\tvoid permanentDeleteById(const(char)[] driveId, const(char)[] id, const(char)[] eTag = null) {\n\t\t// string[string] requestHeaders;\n\t\tconst(char)[] url = driveByIdUrl ~ driveId ~ \"/items/\" ~ id ~ \"/permanentDelete\";\n\t\t//TODO: investigate why this always fail with 412 (Precondition Failed)\n\t\t// if (eTag) requestHeaders[\"If-Match\"] = eTag;\n\t\t// as per documentation, a permanentDelete needs to be a HTTP POST\n\t\tperformPermanentDelete(url);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children\n\tJSONValue createById(string parentDriveId, string parentId, JSONValue item) {\n\t\tstring url = driveByIdUrl ~ parentDriveId ~ \"/items/\" ~ parentId ~ \"/children\";\n\t\treturn post(url, item.toString());\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content\n\tJSONValue simpleUpload(string localPath, string parentDriveId, string parentId, string filename) {\n\t\tstring url = driveByIdUrl ~ parentDriveId ~ \"/items/\" ~ parentId ~ \":/\" ~ encodeComponent(filename) ~ \":/content\";\n\t\treturn put(url, localPath);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content\n\tJSONValue simpleUploadReplace(string localPath, string driveId, string id) {\n\t\tstring url = driveByIdUrl ~ driveId ~ \"/items/\" ~ id ~ \"/content\";\n\t\treturn put(url, localPath);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession\n\t//JSONValue createUploadSession(string parentDriveId, string parentId, string filename, string eTag = null, JSONValue item = null) {\n\tJSONValue createUploadSession(string parentDriveId, string parentId, string filename, const(char)[] eTag = null, JSONValue item = null) {\n\t\tstring[string] requestHeaders;\n\t\tstring url = driveByIdUrl ~ parentDriveId ~ \"/items/\" ~ parentId ~ \":/\" ~ encodeComponent(filename) ~ \":/createUploadSession\";\n\t\t// eTag If-Match header addition commented out for the moment\n\t\t// At some point, post the creation of this upload session the eTag is being 'updated' by OneDrive, thus when uploadFragment() is used\n\t\t// this generates a 412 Precondition Failed and then a 416 Requested Range Not Satisfiable\n\t\t// This needs to be investigated further as to why this occurs\n\t\t\n\t\tif (eTag) requestHeaders[\"If-Match\"] = to!string(eTag);\n\t\treturn post(url, item.toString(), requestHeaders);\n\t}\n\t\n\t// https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#upload-bytes-to-the-upload-session\n\tJSONValue uploadFragment(string uploadUrl, string filepath, long offset, long offsetSize, long fileSize) {\n\t\t// 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\n\t\t// a fragment fails with a 412 Precondition Failed and then a 416 Requested Range Not Satisfiable\n\t\t// For the moment, comment out adding the If-Match header in createUploadSession, which then avoids this issue\n\t\tstring contentRange = \"bytes \" ~ to!string(offset) ~ \"-\" ~ to!string(offset + offsetSize - 1) ~ \"/\" ~ to!string(fileSize);\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"fragment contentRange: \" ~ contentRange, [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Before we submit this 'HTTP PUT' request, pre-emptively check token expiry to avoid future 401s during long uploads\n\t\tcheckAccessTokenExpired();\n\t\t\n\t\t// Perform the HTTP PUT action to upload the file fragment\n\t\treturn put(uploadUrl, filepath, true, contentRange, offset, offsetSize);\n\t}\n\t\n\t// https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#resuming-an-in-progress-upload\n\tJSONValue requestUploadStatus(string uploadUrl) {\n\t\treturn get(uploadUrl, true);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/site_search?view=odsp-graph-online\n\tJSONValue o365SiteSearch(string nextLink) {\n\t\tstring url;\n\t\t// configure URL to query\n\t\tif (nextLink.empty) {\n\t\t\turl = siteSearchUrl ~ \"=*\";\n\t\t} else {\n\t\t\turl = nextLink;\n\t\t}\n\t\treturn get(url);\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_list?view=odsp-graph-online\n\tJSONValue o365SiteDrives(string site_id, string nextLink){\n\t\tstring url;\n\t\t// configure URL to query\n\t\tif (nextLink.empty) {\n\t\t\turl = siteDriveUrl ~ site_id ~ \"/drives\";\n\t\t} else {\n\t\t\turl = nextLink;\n\t\t}\n\t\treturn get(url);\n\t}\n\n\t// Create Webhook Subscription\n\tJSONValue createSubscription(string notificationUrl, SysTime expirationDateTime) {\n\t\tstring driveId;\n\t\tstring url = subscriptionUrl;\n\t\t\n\t\t// What do we set for driveId\n\t\tif (appConfig.getValueString(\"drive_id\").length) {\n\t\t\t// Use the 'config' file option\n\t\t\tdriveId = appConfig.getValueString(\"drive_id\");\n\t\t} else {\n\t\t\t// use appConfig.defaultDriveId\n\t\t\tdriveId = appConfig.defaultDriveId;\n\t\t}\n\t\t\n\t\t// Create a resource item based on if we have a driveId now configured\n\t\tstring resourceItem;\n\t\tif (driveId.length) {\n\t\t\t\tresourceItem = \"/drives/\" ~ driveId ~ \"/root\";\n\t\t} else {\n\t\t\t\tresourceItem = \"/me/drive/root\";\n\t\t}\n\n\t\t// create JSON request to create webhook subscription\n\t\tconst JSONValue request = [\n\t\t\t\"changeType\": \"updated\",\n\t\t\t\"notificationUrl\": notificationUrl,\n\t\t\t\"resource\": resourceItem,\n\t\t\t\"expirationDateTime\": expirationDateTime.toISOExtString(),\n \t\t\t\"clientState\": randomUUID().toString()\n\t\t];\n\t\treturn post(url, request.toString());\n\t}\n\n\t// Renew Webhook Subscription\n\tJSONValue renewSubscription(string subscriptionId, SysTime expirationDateTime) {\n\t\tstring url;\n\t\turl = subscriptionUrl ~ \"/\" ~ subscriptionId;\n\t\tconst JSONValue request = [\n\t\t\t\"expirationDateTime\": expirationDateTime.toISOExtString()\n\t\t];\n\t\treturn patch(url, request.toString(), true);\n\t}\n\n\t// Delete Webhook subscription\n\tvoid deleteSubscription(string subscriptionId) {\n\t\tstring url;\n\t\turl = subscriptionUrl ~ \"/\" ~ subscriptionId;\n\t\tperformDelete(url);\n\t}\n\n\t// Obtain the Websocket Notification URL\n\tJSONValue obtainWebSocketNotificationURL() {\n\t\tif (debugLogging) {addLogEntry(\"Request a Socket.IO Subscription Endpoint: \" ~ websocketEndpoint, [\"debug\"]);}\n\t\treturn get(websocketEndpoint);\n\t}\n\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get_content\n\tvoid downloadById(const(char)[] driveId, const(char)[] itemId, string saveToPath, long fileSize, JSONValue onlineHash, long resumeOffset = 0) {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\t// We pass through to 'downloadFile()'\n\t\t// - resumeOffset\n\t\t// - onlineHash\n\t\t// - driveId\n\t\t// - itemId\n\t\t\n\t\tscope(failure) {\n\t\t\tif (exists(saveToPath)) {\n\t\t\t\t// try and remove the file, catch error\n\t\t\t\ttry {\n\t\t\t\t\tremove(saveToPath);\n\t\t\t\t} catch (FileException exception) {\n\t\t\t\t\t// display the error message\n\t\t\t\t\tdisplayFileSystemErrorMessage(exception.msg, thisFunctionName, saveToPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Create the required local parental path structure if this does not exist\n\t\tstring parentalPath = dirName(saveToPath);\n\n\t\t// Does the parental path exist locally?\n\t\tif (!exists(parentalPath)) {\n\t\t\ttry {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Requested local parental path does not exist, creating directory structure: \" ~ parentalPath, [\"debug\"]);}\n\t\t\t\tmkdirRecurse(parentalPath);\n\t\t\t\t// Has the user disabled the setting of filesystem permissions?\n\t\t\t\tif (!appConfig.getValueBool(\"disable_permission_set\")) {\n\t\t\t\t\t// Configure the applicable permissions for the folder\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting directory permissions for: \" ~ parentalPath, [\"debug\"]);}\n\t\t\t\t\tparentalPath.setAttributes(appConfig.returnRequiredDirectoryPermissions());\n\t\t\t\t} else {\n\t\t\t\t\t// Use inherited permissions\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Using inherited filesystem permissions for: \" ~ parentalPath, [\"debug\"]);}\n\t\t\t\t}\n\t\t\t} catch (FileException exception) {\n\t\t\t\t// display the error message\n\t\t\t\tdisplayFileSystemErrorMessage(exception.msg, thisFunctionName, parentalPath);\n\t\t\t}\n\t\t}\n\n\t\t// Create the URL to download the file\n\t\tconst(char)[] url = driveByIdUrl ~ driveId ~ \"/items/\" ~ itemId ~ \"/content?AVOverride=1\";\n\t\t\n\t\t// Download file using the URL created above\n\t\tdownloadFile(driveId, itemId, url, saveToPath, fileSize, onlineHash, resumeOffset);\n\t\t\n\t\t// Does downloaded file now exist locally?\n\t\tif (exists(saveToPath)) {\n\t\t\t// Has the user disabled the setting of filesystem permissions?\n\t\t\tif (!appConfig.getValueBool(\"disable_permission_set\")) {\n\t\t\t\t// File was downloaded successfully - configure the applicable permissions for the file\n\t\t\t\tif (debugLogging) {addLogEntry(\"Setting file permissions for: \" ~ saveToPath, [\"debug\"]);}\n\t\t\t\tsaveToPath.setAttributes(appConfig.returnRequiredFilePermissions());\n\t\t\t} else {\n\t\t\t\t// Use inherited permissions\n\t\t\t\tif (debugLogging) {addLogEntry(\"Using inherited filesystem permissions for: \" ~ saveToPath, [\"debug\"]);}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Return the actual siteSearchUrl being used and/or requested when performing 'siteQuery = onedrive.o365SiteSearch(nextLink);' call\n\tstring getSiteSearchUrl() {\n\t\treturn siteSearchUrl;\n\t}\n\t\n\t// Private OneDrive API Functions\n\tprivate void addIncludeFeatureRequestHeader(string[string]* headers) {\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Add logging message for OneDrive Personal Accounts\n\t\t\tif (debugLogging) {addLogEntry(\"Adding 'Include-Feature=AddToOneDrive' API request header for OneDrive Personal Account Type\", [\"debug\"]);}\n\t\t} else {\n\t\t\t// Add logging message for OneDrive Business Accounts\n\t\t\tif (debugLogging) {addLogEntry(\"Adding 'Include-Feature=AddToOneDrive' API request header as 'sync_business_shared_items' config option is enabled\", [\"debug\"]);}\n\t\t}\n\t\t// Add feature to request headers\n\t\t(*headers)[\"Prefer\"] = \"Include-Feature=AddToOneDrive\";\n\t}\n\n\tprivate void redeemToken(string authCode) {\n\t\tstring postData =\n\t\t\t\"client_id=\" ~ clientId ~\n\t\t\t\"&redirect_uri=\" ~ encodeComponent(redirectUrl) ~\n\t\t\t\"&code=\" ~ encodeComponent(authCode) ~\n\t\t\t\"&grant_type=authorization_code\";\n\t\tacquireToken(postData.dup);\n\t}\n\t\n\tprivate void acquireToken(char[] postData) {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\t// Configure the response JSON\n\t\tJSONValue response;\n\t\t\n\t\t// Log what we are doing\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"acquireToken: requesting new access token using refresh token (value redacted)\", [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Try and process the 'postData' content\n\t\ttry {\n\t\t\tresponse = post(tokenUrl, postData, null, true, \"application/x-www-form-urlencoded\");\n\t\t} catch (OneDriveException exception) {\n\t\t\t// an error was generated\n\t\t\tif ((exception.httpStatusCode == 400) || (exception.httpStatusCode == 401)) {\n\t\t\t\t// Release curl engine\n\t\t\t\treleaseCurlEngine();\n\t\t\t\t// Handle an unauthorised client\n\t\t\t\thandleClientUnauthorised(exception.httpStatusCode, exception.error);\n\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\tforceExit();\n\t\t\t} else {\n\t\t\t\tif (exception.httpStatusCode >= 500) {\n\t\t\t\t\t// There was a HTTP 5xx Server Side Error - retry\n\t\t\t\t\tacquireToken(postData);\n\t\t\t\t} else {\n\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (response.type() == JSONType.object) {\n\t\t\t// Debug the provided response\n\t\t\tif (debugLogging) {\n\t\t\t\tstring scopes = (\"scope\" in response) ? response[\"scope\"].str() : \"<none>\";\n\t\t\t\tstring tokenType = (\"token_type\" in response) ? response[\"token_type\"].str() : \"<none>\";\n\t\t\t\tlong expiresIn = (\"expires_in\" in response) ? response[\"expires_in\"].integer() : -1;\n\t\t\t\taddLogEntry(\"acquireToken post response: token_type=\" ~ tokenType ~ \", expires_in=\" ~ to!string(expiresIn) ~ \", scope=\" ~ scopes, [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Has the client been configured to use read_only_auth_scope\n\t\t\tif (appConfig.getValueBool(\"read_only_auth_scope\")) {\n\t\t\t\t// read_only_auth_scope has been configured\n\t\t\t\tif (\"scope\" in response){\n\t\t\t\t\tstring effectiveScopes = response[\"scope\"].str();\n\t\t\t\t\t// Display the effective authentication scopes\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Effective API Authentication Scopes: \" ~ effectiveScopes, [\"verbose\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// if we have any write scopes, we need to tell the user to update an remove online prior authentication and exit application\n\t\t\t\t\tif (canFind(effectiveScopes, \"Write\")) {\n\t\t\t\t\t\t// effective scopes contain write scopes .. so not a read-only configuration\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"ERROR: You have authentication scopes that allow write operations. You need to remove your existing application access consent\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"Please login to https://account.live.com/consent/Manage and remove your existing application access consent\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\t// force exit\n\t\t\t\t\t\treleaseCurlEngine();\n\t\t\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\t\t\tforceExit();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\n\t\t\tif (\"access_token\" in response) {\n\t\t\t\t// Process the response JSON\n\t\t\t\tprocessAuthenticationJSON(response);\n\t\t\t} else {\n\t\t\t\t// Release curl engine\n\t\t\t\treleaseCurlEngine();\n\t\t\t\t// Log error message\n\t\t\t\taddLogEntry(\"\\nInvalid authentication response from OneDrive. Please check the response uri\\n\");\n\t\t\t\t// re-authorize\n\t\t\t\tauthorise();\n\t\t\t}\n\t\t} else {\n\t\t\t// Release curl engine\n\t\t\treleaseCurlEngine();\n\t\t\taddLogEntry(\"Invalid response from the Microsoft Graph API. Unable to initialise OneDrive API instance.\");\n\t\t\t// Must force exit here, allow logging to be done\n\t\t\tforceExit();\n\t\t}\n\t}\n\t\t\n\t// Process the authentication JSON\n\tprivate void processAuthenticationJSON(JSONValue response) {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\n\t\t// Use 'access_token' and set in the application configuration\n\t\tappConfig.accessToken = \"bearer \" ~ strip(response[\"access_token\"].str);\n\t\t\t\t\n\t\t// Do we print the current access token\n\t\tdebugOutputAccessToken();\n\t\t\n\t\t// Obtain the 'refresh_token' and its expiry\n\t\trefreshToken = strip(response[\"refresh_token\"].str);\n\t\tappConfig.accessTokenExpiration = Clock.currTime() + dur!\"seconds\"(response[\"expires_in\"].integer());\n\t\t\n\t\t// Debug this response\n\t\tif (debugLogging) {addLogEntry(\"appConfig.accessTokenExpiration = \" ~ to!string(appConfig.accessTokenExpiration), [\"debug\"]);}\n\t\t\n\t\tif (!dryRun) {\n\t\t\t// Update the refreshToken in appConfig so that we can reuse it\n\t\t\tif (appConfig.refreshToken.empty) {\n\t\t\t\t// The access token is empty\n\t\t\t\tif (debugLogging) {addLogEntry(\"Updating appConfig.refreshToken with new refreshToken as appConfig.refreshToken is empty\", [\"debug\"]);}\n\t\t\t\tappConfig.refreshToken = refreshToken;\n\t\t\t} else {\n\t\t\t\t// Is the access token different?\n\t\t\t\tif (appConfig.refreshToken != refreshToken) {\n\t\t\t\t\t// Update the memory version\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updating appConfig.refreshToken with updated refreshToken\", [\"debug\"]);}\n\t\t\t\t\tappConfig.refreshToken = refreshToken;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// try and update the 'refresh_token' file on disk\n\t\t\ttry {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Updating 'refresh_token' on disk\", [\"debug\"]);}\n\t\t\t\tstd.file.write(appConfig.refreshTokenFilePath, refreshToken);\n\t\t\t\tif (debugLogging) {addLogEntry(\"Setting file permissions for: \" ~ appConfig.refreshTokenFilePath, [\"debug\"]);}\n\t\t\t\tappConfig.refreshTokenFilePath.setAttributes(appConfig.returnSecureFilePermission());\n\t\t\t} catch (FileException exception) {\n\t\t\t\t// display the error message\n\t\t\t\tdisplayFileSystemErrorMessage(exception.msg, thisFunctionName, appConfig.refreshTokenFilePath);\n\t\t\t}\n\t\t}\n\t}\n\t\n\tprivate void generateNewAccessToken() {\n\t\tif (debugLogging) {addLogEntry(\"Need to generate a new access token for Microsoft OneDrive\", [\"debug\"]);}\n\t\t// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session\n\t\tif (appConfig.getValueBool(\"use_intune_sso\")) {\n\t\t\t// The client is configured to use Intune SSO via Microsoft Identity Broker dbus session\n\t\t\tauto intuneAuthResult = acquire_token_silently(appConfig.intuneAccountDetails, appConfig.getValueString(\"application_id\"));\n\t\t\tJSONValue intuneBrokerJSONData = intuneAuthResult.brokerTokenResponse;\n\t\t\t// Is the JSON data valid?\n\t\t\tif ((intuneBrokerJSONData.type() == JSONType.object)) {\n\t\t\t\t// Does the JSON data have the required renewal elements:\n\t\t\t\t// - accessToken\n\t\t\t\t// - account\n\t\t\t\t// - expiresOn\n\t\t\t\tif ((hasAccessTokenData(intuneBrokerJSONData)) && (hasAccountData(intuneBrokerJSONData)) && (hasExpiresOn(intuneBrokerJSONData))) {\n\t\t\t\t\t// Details exist\n\t\t\t\t\tprocessIntuneResponse(intuneBrokerJSONData);\n\t\t\t\t} else {\n\t\t\t\t\t// no ... expected values not available\n\t\t\t\t\taddLogEntry(\"Required Intune JSON elements are not present in the Intune JSON response\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Not a valid JSON response\n\t\t\t\taddLogEntry(\"Invalid Intune JSON response when attempting access token renewal\");\n\t\t\t}\n\t\t} else {\n\t\t\t// Normal authentication method \n\t\t\tauto postData = appender!(string)();\n\t\t\tpostData ~= \"client_id=\" ~ clientId;\n\t\t\tpostData ~= \"&redirect_uri=\" ~ redirectUrl;\n\t\t\tpostData ~= \"&refresh_token=\" ~ to!string(refreshToken);\n\t\t\tpostData ~= \"&grant_type=refresh_token\";\n\t\t\tacquireToken(postData.data.dup);\n\t\t}\n\t}\n\t\n\t// Check if the existing access token has expired, if it has, generate a new one\n\tprivate void checkAccessTokenExpired() {\n\t\tif (Clock.currTime() >= appConfig.accessTokenExpiration) {\n\t\t\tif (debugLogging) {addLogEntry(\"Microsoft OneDrive OAuth2 Access Token has expired. Must generate a new Microsoft OneDrive OAuth2 Access Token\", [\"debug\"]);}\n\t\t\tgenerateNewAccessToken();\n\t\t} else {\n\t\t\tif (debugLogging) {addLogEntry(\"Microsoft OneDrive OAuth2 Access Token Valid Until (Local): \" ~ to!string(appConfig.accessTokenExpiration), [\"debug\"]);}\n\t\t}\n\t}\n\t\n\tprivate string getAccessToken() {\n\t\tcheckAccessTokenExpired();\n\t\treturn to!string(appConfig.accessToken);\n\t}\n\n\tprivate void addAccessTokenHeader(string[string]* requestHeaders) {\n\t\t(*requestHeaders)[\"Authorization\"] = getAccessToken();\n\t}\n\t\n\tprivate void connect(HTTP.Method method, const(char)[] url, bool skipToken, CurlResponse response, string[string] requestHeaders=null) {\n\t\t// If we are debug logging, output the URL being accessed and the HTTP method being used to access that URL\n\t\tif (debugLogging) {addLogEntry(\"HTTP \" ~ to!string(method) ~ \" request to URL: \" ~ to!string(url), [\"debug\"]);}\n\t\t\n\t\t// Check access token first in case the request is overridden\n\t\tif (!skipToken) addAccessTokenHeader(&requestHeaders);\n\t\tcurlEngine.setResponseHolder(response);\n\t\tforeach(k, v; requestHeaders) {\n\t\t\tcurlEngine.addRequestHeader(k, v);\n\t\t}\n\t\tcurlEngine.connect(method, url);\n\t}\n\n\tprivate void performDelete(const(char)[] url, string[string] requestHeaders=null, string callingFunction=__FUNCTION__, int lineno=__LINE__) {\n\t\tbool validateJSONResponse = false;\n\t\toneDriveErrorHandlerWrapper((CurlResponse response) {\n\t\t\tconnect(HTTP.Method.del, url, false, response, requestHeaders);\n\t\t\treturn curlEngine.execute();\n\t\t}, validateJSONResponse, callingFunction, lineno);\n\t}\n\t\n\tprivate void performPermanentDelete(const(char)[] url, string[string] requestHeaders=null, string callingFunction=__FUNCTION__, int lineno=__LINE__) {\n\t\tbool validateJSONResponse = false;\n\t\toneDriveErrorHandlerWrapper((CurlResponse response) {\n\t\t\tconnect(HTTP.Method.post, url, false, response, requestHeaders);\n\t\t\tcurlEngine.setZeroContentLength();\n\t\t\treturn curlEngine.execute();\n\t\t}, validateJSONResponse, callingFunction, lineno);\n\t}\n\t\n\t// Download a file based on the URL request\n\tprivate void downloadFile(const(char)[] driveId, const(char)[] itemId, const(char)[] url, string filename, long fileSize, JSONValue onlineHash, long resumeOffset = 0, string callingFunction=__FUNCTION__, int lineno=__LINE__) {\n\t\t// Threshold for displaying download bar\n\t\tlong thresholdFileSize = 4 * 2^^20; // 4 MiB\n\n\t\t// To support marking of partially-downloaded files\n\t\tstring originalFilename = filename;\n\t\tstring downloadFilename = filename ~ \".partial\";\n\n\t\t// To support resumable downloads, configure the 'resumable data' file path\n\t\tstring threadResumeDownloadFilePath = appConfig.resumeDownloadFilePath ~ \".\" ~ generateAlphanumericString();\n\n\t\t// Create a JSONValue with download state so this can be used when resuming, to evaluate if the online file has changed, and if we are able to resume in a safe manner\n\t\tJSONValue resumeDownloadData = JSONValue([\n\t\t\t\"driveId\":          JSONValue(to!string(driveId)),\n\t\t\t\"itemId\":           JSONValue(to!string(itemId)),\n\t\t\t\"onlineHash\":       onlineHash,\n\t\t\t\"originalFilename\": JSONValue(originalFilename),\n\t\t\t\"downloadFilename\": JSONValue(downloadFilename),\n\t\t\t\"resumeOffset\":     JSONValue(to!string(resumeOffset))\n\t\t]);\n\n\t\t// ----------------------------------------------------------------------\n\t\t// Progress state – must live for the whole downloadFile() call so that\n\t\t// retries triggered by oneDriveErrorHandlerWrapper() do NOT reset the\n\t\t// visible progress bar back to 0%.\n\t\t// ----------------------------------------------------------------------\n\t\tsize_t expected_total_segments = 20;\n\n\t\tSysTime startTime = Clock.currTime();\n\t\tlong   start_unix_time = startTime.toUnixTime();\n\t\tint    h, m, s;\n\t\tstring etaString;\n\t\tbool   barInit = false;\n\t\treal   previousProgressPercent = 0.0; // last *displayed* percent\n\t\treal   percentCheck = 5.0;\n\t\tsize_t segmentCount = 0;\n\n\t\t// Validate the JSON response\n\t\tbool validateJSONResponse = false;\n\n\t\toneDriveErrorHandlerWrapper((CurlResponse response) {\n\t\t\tconnect(HTTP.Method.get, url, false, response);\n\n\t\t\tif (fileSize >= thresholdFileSize) {\n\t\t\t\t// ------------------------------------------------------------------\n\t\t\t\t// Determine an effective resume offset for this attempt.\n\t\t\t\t//\n\t\t\t\t//  - Start from the passed-in resumeOffset (from resume_download.*)\n\t\t\t\t//  - If a .partial file exists and is larger, prefer its size.\n\t\t\t\t//    This ensures we never re-download bytes we already have on disk.\n\t\t\t\t// ------------------------------------------------------------------\n\t\t\t\tlong effectiveResumeOffset = resumeOffset;\n\n\t\t\t\tif (exists(downloadFilename)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tauto partialSize = cast(long) getSize(downloadFilename);\n\t\t\t\t\t\tif (partialSize > effectiveResumeOffset) {\n\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\taddLogEntry(\n\t\t\t\t\t\t\t\t\t\"Resumable download: detected existing partial file '\" ~ downloadFilename ~\n\t\t\t\t\t\t\t\t\t\"' of size \" ~ to!string(partialSize) ~ \" bytes\",\n\t\t\t\t\t\t\t\t\t[\"debug\"]\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\taddLogEntry(\n\t\t\t\t\t\t\t\t\t\"Adjusting resumable offset for '\" ~ originalFilename ~\n\t\t\t\t\t\t\t\t\t\"' from \" ~ to!string(effectiveResumeOffset) ~\n\t\t\t\t\t\t\t\t\t\" to \" ~ to!string(partialSize),\n\t\t\t\t\t\t\t\t\t[\"debug\"]\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\teffectiveResumeOffset = partialSize;\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (FileException ex) {\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\n\t\t\t\t\t\t\t\t\"Failed to obtain size of partial download file '\" ~ downloadFilename ~\n\t\t\t\t\t\t\t\t\"': \" ~ ex.msg,\n\t\t\t\t\t\t\t\t[\"debug\"]\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// If we have a resumable offset to use, set this as the offset to use\n\t\t\t\tif (effectiveResumeOffset > 0) {\n\t\t\t\t\tcurlEngine.setDownloadResumeOffset(effectiveResumeOffset);\n\n\t\t\t\t\t// Keep the JSON state in sync with the absolute offset\n\t\t\t\t\tresumeDownloadData[\"resumeOffset\"] = JSONValue(to!string(effectiveResumeOffset));\n\t\t\t\t}\n\n\t\t\t\t// Setup progress bar to display\n\t\t\t\tcurlEngine.http.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) {\n\t\t\t\t\tstring downloadLogEntry = \"Downloading: \" ~ filename ~ \" ... \";\n\n\t\t\t\t\t// ------------------------------------------------------------------\n\t\t\t\t\t// Compute absolute progress as bytes_on_disk + bytes_this_transfer.\n\t\t\t\t\t// This ensures that after a retry, the percentage continues from\n\t\t\t\t\t// (for example) 25% instead of restarting at 0%.\n\t\t\t\t\t// ------------------------------------------------------------------\n\t\t\t\t\tlong absoluteNow = effectiveResumeOffset + cast(long)dlnow;\n\n\t\t\t\t\tlong absoluteTotal;\n\t\t\t\t\tif (fileSize > 0) {\n\t\t\t\t\t\tabsoluteTotal = fileSize;\n\t\t\t\t\t} else if (dltotal > 0) {\n\t\t\t\t\t\tabsoluteTotal = effectiveResumeOffset + cast(long)dltotal;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tabsoluteTotal = absoluteNow; // best effort; avoids div-by-zero\n\t\t\t\t\t}\n\n\t\t\t\t\tif (absoluteTotal <= 0) {\n\t\t\t\t\t\tabsoluteTotal = 1; // safety guard\n\t\t\t\t\t}\n\n\t\t\t\t\t// Floor to nearest whole number\n\t\t\t\t\treal currentDLPercent = floor(\n\t\t\t\t\t\t(cast(real) absoluteNow / cast(real) absoluteTotal) * 100.0\n\t\t\t\t\t);\n\n\t\t\t\t\t// Clamp just in case\n\t\t\t\t\tif (currentDLPercent < 0.0) {\n\t\t\t\t\t\tcurrentDLPercent = 0.0;\n\t\t\t\t\t} else if (currentDLPercent > 100.0) {\n\t\t\t\t\t\tcurrentDLPercent = 100.0;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Debug logging (optional, but handy while we’re testing)\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"\", [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"absoluteNow      = \" ~ to!string(absoluteNow), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"absoluteTotal    = \" ~ to!string(absoluteTotal), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"Percent Complete = \" ~ to!string(currentDLPercent), [\"debug\"]);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Have we started downloading (in absolute terms)?\n\t\t\t\t\tif (currentDLPercent > 0) {\n\t\t\t\t\t\t// Has the user set a data rate limit?\n\t\t\t\t\t\t// when using rate_limit, we will get odd download rates, for example:\n\t\t\t\t\t\t// Percent Complete = 24\n\t\t\t\t\t\t// Data Received    = 13080163\n\t\t\t\t\t\t// Expected Total   = 52428800\n\t\t\t\t\t\t// Percent Complete = 24\n\t\t\t\t\t\t// Data Received    = 13685777\n\t\t\t\t\t\t// Expected Total   = 52428800\n\t\t\t\t\t\t// Percent Complete = 26   <---- jumps to 26% missing 25%, thus fmod misses incrementing progress bar\n\t\t\t\t\t\t// Data Received    = 13685777\n\t\t\t\t\t\t// Expected Total   = 52428800\n\t\t\t\t\t\t// Percent Complete = 26\n\n\t\t\t\t\t\tif (appConfig.getValueLong(\"rate_limit\") > 0) {\n\t\t\t\t\t\t\t// Under rate limiting, libcurl can \"jump\" the visible percentage,\n\t\t\t\t\t\t\t// e.g. 24% -> 26%, which can skip a clean 5% boundary.\n\t\t\t\t\t\t\t// To keep a stable 5% display (5, 10, 15, ...), we use a\n\t\t\t\t\t\t\t// catch-up loop that prints every missing 5% step up to\n\t\t\t\t\t\t\t// currentDLPercent, based on the *absolute* percentage.\n\n\t\t\t\t\t\t\treal nextPercent = previousProgressPercent + percentCheck;\n\n\t\t\t\t\t\t\t// Emit all missing 5% steps below 100%\n\t\t\t\t\t\t\twhile (nextPercent < 100.0 && currentDLPercent >= nextPercent) {\n\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Incrementing Progress Bar (rate_limit) to \" ~ to!string(nextPercent) ~ \"%\", [\"debug\"]);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tsegmentCount++;\n\t\t\t\t\t\t\t\tetaString = formatETA(calc_eta(segmentCount, expected_total_segments, start_unix_time));\n\t\t\t\t\t\t\t\tstring percentage = leftJustify(to!string(cast(int) nextPercent) ~ \"%\", 5, ' ');\n\t\t\t\t\t\t\t\taddLogEntry(downloadLogEntry ~ percentage ~ etaString, [\"consoleOnly\"]);\n\n\t\t\t\t\t\t\t\tpreviousProgressPercent = nextPercent;\n\t\t\t\t\t\t\t\tnextPercent += percentCheck;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Handle 100% exactly once\n\t\t\t\t\t\t\tif ((currentDLPercent >= 100.0) && (previousProgressPercent < 100.0)) {\n\t\t\t\t\t\t\t\tSysTime endTime = Clock.currTime();\n\t\t\t\t\t\t\t\tlong end_unix_time = endTime.toUnixTime();\n\t\t\t\t\t\t\t\tint download_duration = cast(int)(end_unix_time - start_unix_time);\n\t\t\t\t\t\t\t\tdur!\"seconds\"(download_duration).split!(\"hours\", \"minutes\", \"seconds\")(h, m, s);\n\t\t\t\t\t\t\t\tetaString = format!\"| DONE in %02d:%02d:%02d\"(h, m, s);\n\t\t\t\t\t\t\t\tstring percentage = leftJustify(\"100%\", 5, ' ');\n\t\t\t\t\t\t\t\taddLogEntry(downloadLogEntry ~ percentage ~ etaString, [\"consoleOnly\"]);\n\n\t\t\t\t\t\t\t\tpreviousProgressPercent = 100.0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Non-rate-limited case: fmod-based behaviour but applied to the absolute percentage\n\t\t\t\t\t\t\tif ((isIdentical(fmod(currentDLPercent, percentCheck), 0.0)) &&\n\t\t\t\t\t\t\t\t(previousProgressPercent != currentDLPercent)) {\n\t\t\t\t\t\t\t\t// currentDLPercent matches a new increment\n\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Incrementing Progress Bar using fmod match\", [\"debug\"]);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (currentDLPercent != 100) {\n\t\t\t\t\t\t\t\t\t// Not 100% yet\n\t\t\t\t\t\t\t\t\tsegmentCount++;\n\t\t\t\t\t\t\t\t\tetaString = formatETA(calc_eta(segmentCount, expected_total_segments, start_unix_time));\n\t\t\t\t\t\t\t\t\tstring percentage = leftJustify(to!string(cast(int) currentDLPercent) ~ \"%\", 5, ' ');\n\t\t\t\t\t\t\t\t\taddLogEntry(downloadLogEntry ~ percentage ~ etaString, [\"consoleOnly\"]);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// 100% done\n\t\t\t\t\t\t\t\t\tSysTime endTime = Clock.currTime();\n\t\t\t\t\t\t\t\t\tlong end_unix_time = endTime.toUnixTime();\n\t\t\t\t\t\t\t\t\tint download_duration = cast(int)(end_unix_time - start_unix_time);\n\t\t\t\t\t\t\t\t\tdur!\"seconds\"(download_duration).split!(\"hours\", \"minutes\", \"seconds\")(h, m, s);\n\t\t\t\t\t\t\t\t\tetaString = format!\"| DONE in %02d:%02d:%02d\"(h, m, s);\n\t\t\t\t\t\t\t\t\tstring percentage = leftJustify(\"100%\", 5, ' ');\n\t\t\t\t\t\t\t\t\taddLogEntry(downloadLogEntry ~ percentage ~ etaString, [\"consoleOnly\"]);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tpreviousProgressPercent = currentDLPercent;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Has our absolute offset advanced?\n\t\t\t\t\t\tif (absoluteNow > to!long(resumeDownloadData[\"resumeOffset\"].str)) {\n\t\t\t\t\t\t\t// Update resumeOffset for this progress event with the latest absolute offset\n\t\t\t\t\t\t\tresumeDownloadData[\"resumeOffset\"] = JSONValue(to!string(absoluteNow));\n\n\t\t\t\t\t\t\t// Save resumable download data - this needs to be saved on every onProgress event that is processed\n\t\t\t\t\t\t\tsaveResumeDownloadFile(threadResumeDownloadFilePath, resumeDownloadData);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// We may get frequent progress callbacks at 0%, make sure we initialise the bar once per overall download\n\t\t\t\t\t\tif ((currentDLPercent == 0) && (!barInit)) {\n\t\t\t\t\t\t\tetaString = \"|  ETA    --:--:--\";\n\t\t\t\t\t\t\tstring percentage = leftJustify(\"0%\", 5, ' ');\n\t\t\t\t\t\t\taddLogEntry(downloadLogEntry ~ percentage ~ etaString, [\"consoleOnly\"]);\n\t\t\t\t\t\t\tbarInit = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn 0;\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\t// No progress bar, no resumable download\n\t\t\t}\n\n\t\t\t// Capture the result of the download action\n\t\t\tauto result = curlEngine.download(originalFilename, downloadFilename);\n\n\t\t\t// Safe remove 'threadResumeDownloadFilePath' as if we get to this point, the file has been downloaded successfully\n\t\t\tsafeRemove(threadResumeDownloadFilePath);\n\n\t\t\t// Reset this curlEngine offset value now that the file has been downloaded successfully\n\t\t\tcurlEngine.resetDownloadResumeOffset();\n\n\t\t\t// Return the applicable result\n\t\t\treturn result;\n\t\t}, validateJSONResponse, callingFunction, lineno);\n\t}\n\n\t// Save the resume download data\n\tprivate void saveResumeDownloadFile(string threadResumeDownloadFilePath, JSONValue resumeDownloadData) {\n\t\t// Set this function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t\n\t\ttry {\n\t\t\tstd.file.write(threadResumeDownloadFilePath, resumeDownloadData.toString());\n\t\t} catch (FileException e) {\n\t\t\t// display the error message\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, threadResumeDownloadFilePath);\n\t\t}\n\t}\n\n\tprivate JSONValue get(string url, bool skipToken = false, string[string] requestHeaders=null, string callingFunction=__FUNCTION__, int lineno=__LINE__) {\n\t\tbool validateJSONResponse = true;\n\t\treturn oneDriveErrorHandlerWrapper((CurlResponse response) {\n\t\t\tconnect(HTTP.Method.get, url, skipToken, response, requestHeaders);\n\t\t\treturn curlEngine.execute();\n\t\t}, validateJSONResponse, callingFunction, lineno);\n\t}\n\n\tprivate JSONValue patch(const(char)[] url, const(char)[] patchData, bool validateJSONResponseInput, string[string] requestHeaders=null, const(char)[] contentType = \"application/json\", string callingFunction=__FUNCTION__, int lineno=__LINE__) {\n\t\tbool validateJSONResponse = validateJSONResponseInput;\n\t\treturn oneDriveErrorHandlerWrapper((CurlResponse response) {\n\t\t\tconnect(HTTP.Method.patch, url, false, response, requestHeaders);\n\t\t\tcurlEngine.setContent(contentType, patchData);\n\t\t\treturn curlEngine.execute();\n\t\t}, validateJSONResponse, callingFunction, lineno);\n\t}\n\n\tprivate 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__) {\n\t\tbool validateJSONResponse = true;\n\t\treturn oneDriveErrorHandlerWrapper((CurlResponse response) {\n\t\t\tconnect(HTTP.Method.post, url, skipToken, response, requestHeaders);\n\t\t\tcurlEngine.setContent(contentType, postData);\n\t\t\treturn curlEngine.execute();\n\t\t}, validateJSONResponse, callingFunction, lineno);\n\t}\n\t\n\tprivate 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__) {\n\t\tbool validateJSONResponse = true;\n\t\treturn oneDriveErrorHandlerWrapper((CurlResponse response) {\n\t\t\tconnect(HTTP.Method.put, url, skipToken, response);\n\t\t\tcurlEngine.setFile(filepath, contentRange, offset, offsetSize);\n\t\t\treturn curlEngine.execute();\n\t\t}, validateJSONResponse, callingFunction, lineno);\n\t}\n\n\t// Wrapper function for all requests to OneDrive API\n\t// - This should throw a OneDriveException so that this exception can be handled appropriately elsewhere in the application\n\tprivate JSONValue oneDriveErrorHandlerWrapper(CurlResponse delegate(CurlResponse response) executer, bool validateJSONResponse, string callingFunction, int lineno) {\n\t\t// Create a new 'curl' response\n\t\tresponse = new CurlResponse();\n\t\t\n\t\t// Other wrapper variables\n\t\tint retryAttempts = 0;\n\t\tint baseBackoffInterval = 1; // Base backoff interval in seconds\n\t\tint maxRetryCount = 175200; // Approx 365 days based on maxBackoffInterval + appConfig.defaultDataTimeout\n\t\t//int maxRetryCount = 5; // Temp\n\t\tint maxBackoffInterval = 120; // Maximum backoff interval in seconds\n\t\tint thisBackOffInterval = 0;\n\t\tint timestampAlign = 0;\n\t\tJSONValue result;\n\t\tSysTime currentTime;\n\t\tSysTime retryTime;\n\t\tbool retrySuccess = false;\n\t\tbool transientError = false;\n\t\tbool sslVerifyPeerDisabled = false;\n\t\t\n\t\twhile (!retrySuccess) {\n\t\t\t// Reset thisBackOffInterval\n\t\t\tthisBackOffInterval = 0;\n\t\t\ttransientError = false;\n\t\t\t\n\t\t\tif (retryAttempts >= 1) {\n\t\t\t\t// re-try log entry & clock time\n\t\t\t\tretryTime = Clock.currTime();\n\t\t\t\tretryTime.fracSecs = Duration.zero;\n\t\t\t\taddLogEntry(\"Retrying the respective Microsoft Graph API call for Internal Thread ID: \" ~ to!string(curlEngine.internalThreadId) ~ \" (Timestamp: \" ~ to!string(retryTime) ~ \") ...\");\n\t\t\t}\n\t\t\n\t\t\ttry {\n\t\t\t\tresponse.reset();\n\t\t\t\tresponse = executer(response);\n\t\t\t\t// Check for a valid response\n\t\t\t\tif (response.hasResponse) {\n\t\t\t\t\t// Process the response\n\t\t\t\t\tresult = response.json();\n\t\t\t\t\t// Print response if 'debugHTTPSResponse' is flagged\n\t\t\t\t\tif (debugHTTPSResponse){\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Microsoft Graph API Response: \" ~ response.dumpResponse(), [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Check http response code, raise a OneDriveException if the operation was not successfully performed\n\t\t\t\t\tif (checkHttpResponseCode(response.statusLine.code)) {\n\t\t\t\t\t\t// 'curl' on platforms like Ubuntu does not reliably provide the 'http.statusLine.reason' when using HTTP/2\n\t\t\t\t\t\t// This is a curl bug, but because Ubuntu uses old packages and never updates them, we are stuck with working around this bug\n\t\t\t\t\t\tif (response.statusLine.reason.length == 0) {\n\t\t\t\t\t\t\t// No 'reason', fetch what it should have been\n\t\t\t\t\t\t\tresponse.statusLine.reason = getMicrosoftGraphStatusMessage(response.statusLine.code);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\tif (response.statusLine.code != 404) {\n\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\taddLogEntry(\"response.statusLine.code: \" ~ to!string(response.statusLine.code), [\"debug\"]);\n\t\t\t\t\t\t\t\taddLogEntry(\"response.statusLine.reason: \" ~ to!string(response.statusLine.reason), [\"debug\"]);\n\t\t\t\t\t\t\t\taddLogEntry(\"actual curl response: \" ~ to!string(response), [\"debug\"]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// 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.\n\t\t\t\t\t\tthrow new OneDriveException(response.statusLine.code, response.statusLine.reason, response);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Do we need to validate the JSON response?\n\t\t\t\t\tif (validateJSONResponse) {\n\t\t\t\t\t\tconst code = response.statusLine.code;\n\n\t\t\t\t\t\t// 204 = No Content is a valid success response for some Graph operations (e.g. PATCH/DELETE).\n\t\t\t\t\t\t// In that case, there is no JSON payload to validate.\n\t\t\t\t\t\tif (code != 204) {\n\t\t\t\t\t\t\t// If caller expects JSON, an empty body is not acceptable\n\t\t\t\t\t\t\tif (response.content.length == 0) {\n\t\t\t\t\t\t\t\tthrow new OneDriveException( 0, \"Caller requested a JSON object response, but the response body was empty\", response);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Body is present: it must be a JSON object\n\t\t\t\t\t\t\tif (result.type() != JSONType.object) {\n\t\t\t\t\t\t\t\tthrow new OneDriveException(0, \"Caller requested a JSON object response, but the response was not a JSON object\", response);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// If we get to this point, there is no error from http.perform() on re-try\n\t\t\t\t\t// If retryAttempts is greater than 1, it means we were re-trying the request\n\t\t\t\t\tif (retryAttempts > 1) {\n\t\t\t\t\t\t// unset the fresh connect option as this then creates performance issues if left enabled\n\t\t\t\t\t\tunsetFreshConnectOption();\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// On successful http.perform() processing, break out of the loop\n\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\t// Throw a custom 506 error\n\t\t\t\t\t// 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 \n\t\t\t\t\t// interpreted to signal that the response received didn't meet the expected criteria or format.\n\t\t\t\t\tthrow new OneDriveException(506, \"Received an unexpected response from Microsoft OneDrive\", response);\n\t\t\t\t}\n\t\t\t// A 'curl' exception was thrown \n\t\t\t} catch (CurlException exception) {\n\t\t\t\t// Handle 'curl' exception errors\n\t\t\t\t\n\t\t\t\t// Detail the curl exception, debug output only\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"Handling a curl exception:\", [\"debug\"]);\n\t\t\t\t\taddLogEntry(to!string(response), [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Parse and display error message received from OneDrive\n\t\t\t\tif (debugLogging) {addLogEntry(callingFunction ~ \"() - Generated a OneDrive CurlException\", [\"debug\"]);}\n\t\t\t\tauto errorArray = splitLines(exception.msg);\n\t\t\t\tstring errorMessage = errorArray[0];\n\n\t\t\t\t// Configure libcurl to perform a fresh connection\n\t\t\t\tsetFreshConnectOption();\n\n\t\t\t\t// What is contained in the curl error message?\n\t\t\t\t// Handle the following:\n\t\t\t\t// - Couldn't connect to server on handle\n\t\t\t\t// - Could not connect to server on handle (changed noticed in curl 8.14.1, possibly done earlier ...)\n\t\t\t\t// - Couldn't resolve host name on handle\n\t\t\t\t// - Could not resolve host name on handle (changed noticed in curl 8.14.1, possibly done earlier ...)\n\t\t\t\t// - Timeout was reached on handle\n\t\t\t\tif (canFind(errorMessage, \"connect to server on handle\") || canFind(errorMessage, \"resolve host name on handle\") || canFind(errorMessage, \"resolve hostname on handle\") || canFind(errorMessage, \"Timeout was reached on handle\")) {\n\t\t\t\t\t// Connectivity to Microsoft OneDrive was lost\n\t\t\t\t\taddLogEntry(\"Internet connectivity to Microsoft OneDrive service has been interrupted .. re-trying in the background\");\n\t\t\t\t\t\n\t\t\t\t\t// What caused the initial curl exception?\n\t\t\t\t\t// - DNS resolution issue\n\t\t\t\t\tif (canFind(errorMessage, \"resolve host name on handle\")) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Unable to resolve server - DNS access blocked?\", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t// - connection issue\n\t\t\t\t\tif (canFind(errorMessage, \"connect to server on handle\")) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Unable to connect to server - HTTPS access blocked?\", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t// - timeout issue\n\t\t\t\t\tif (canFind(errorMessage, \"Timeout was reached on handle\")) {\n\t\t\t\t\t\t// Common cause is libcurl trying IPv6 DNS resolution when there are only IPv4 DNS servers available\n\t\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"A libcurl timeout has been triggered - data transfer too slow, no DNS resolution response, no server response or operational timeout\", [\"verbose\"]);\n\t\t\t\t\t\t\t// There are 3 common causes for this issue:\n\t\t\t\t\t\t\t// 1. Usually poor DNS resolution where libcurl flip/flops to use IPv6 and is unable to resolve\n\t\t\t\t\t\t\t// 2. A device between the user and Microsoft OneDrive is unable to correctly handle HTTP/2 communication\n\t\t\t\t\t\t\t// 3. No Internet access from this system at this point in time\n\t\t\t\t\t\t\taddLogEntry(\" - IPv6 DNS resolution issues may be causing timeouts. Consider setting 'ip_protocol_version' to IPv4 to potentially avoid this\", [\"verbose\"]);\n\t\t\t\t\t\t\taddLogEntry(\" - 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\"]);\n\t\t\t\t\t\t\taddLogEntry(\" - Ensure 'operation_timeout' is configured for the conditions of your network, covering DNS lookups, connection setup, TLS negotiation, and how long data transfers normally take\", [\"verbose\"]);\n\t\t\t\t\t\t\taddLogEntry(\" - If these options do not resolve this timeout issue, please use --debug-https to diagnose this issue further.\", [\"verbose\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Some other 'libcurl' error was returned\n\t\t\t\t\tif (canFind(errorMessage, \"Problem with the SSL CA cert (path? access rights?) on handle\")) {\n\t\t\t\t\t\t// error setting certificate verify locations:\n\t\t\t\t\t\t//  CAfile: /etc/pki/tls/certs/ca-bundle.crt\n\t\t\t\t\t\t//\tCApath: none\n\t\t\t\t\t\t// \n\t\t\t\t\t\t// Tell the Curl Engine to bypass SSL check - essentially SSL is passing back a bad value due to 'stdio' compile time option\n\t\t\t\t\t\t// Further reading:\n\t\t\t\t\t\t//  https://github.com/curl/curl/issues/6090\n\t\t\t\t\t\t//  https://github.com/openssl/openssl/issues/7536\n\t\t\t\t\t\t//  https://stackoverflow.com/questions/45829588/brew-install-fails-curl77-error-setting-certificate-verify\n\t\t\t\t\t\t//  https://forum.dlang.org/post/vwvkbubufexgeuaxhqfl@forum.dlang.org\n\t\t\t\t\t\t\n\t\t\t\t\t\tstring sslCertReadErrorMessage = \"System SSL CA certificates are missing or unreadable by libcurl – please ensure the correct CA bundle is installed and is accessible.\";\n\t\t\t\t\t\taddLogEntry(\"ERROR: \" ~ sslCertReadErrorMessage);\n\t\t\t\t\t\tthrow new OneDriveError(sslCertReadErrorMessage);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Was this a curl initialization error?\n\t\t\t\t\t\tif (canFind(errorMessage, \"Failed initialization on handle\")) {\n\t\t\t\t\t\t\t// initialization error ... prevent a run-away process if we have zero disk space\n\t\t\t\t\t\t\tulong localActualFreeSpace = getAvailableDiskSpace(\".\");\n\t\t\t\t\t\t\tif (localActualFreeSpace == 0) {\n\t\t\t\t\t\t\t\tthrow new OneDriveError(\"Zero disk space detected\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Unknown curl error\n\t\t\t\t\t\t\tdisplayGeneralErrorMessage(exception, callingFunction, lineno);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Fallback: Ensure retry interval is enforced in case of unknown CurlException\n\t\t\t\t\t\t\tif (thisBackOffInterval == 0) {\n\t\t\t\t\t\t\t\tthisBackOffInterval = calculateBackoff(retryAttempts, baseBackoffInterval, maxBackoffInterval);\n\t\t\t\t\t\t\t\tif (thisBackOffInterval <= 0) {\n\t\t\t\t\t\t\t\t\tthisBackOffInterval = 1;\n\t\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: Enforcing minimum backoff interval of 1 second – unclassified CurlException\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t// A OneDrive API exception was thrown\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// https://developer.overdrive.com/docs/reference-guide\n\t\t\t\t// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/concepts/errors?view=odsp-graph-online\n\t\t\t\t// https://learn.microsoft.com/en-us/graph/errors\n\t\t\t\t\n\t\t\t\t/**\n\t\t\t\t\tHTTP/1.1 Response handling\n\n\t\t\t\t\tErrors 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.\n\n\t\t\t\t\tStatus code\t\tStatus message\t\t\t\t\t\tDescription\n\t\t\t\t\t100\t\t\t\tContinue\t\t\t\t\t\t\tContinue\n\t\t\t\t\t200 \t\t\tOK\t\t\t\t\t\t\t\t\tRequest was handled OK\n\t\t\t\t\t201 \t\t\tCreated\t\t\t\t\t\t\t\tThis means you've made a successful POST to checkout, lock in a format, or place a hold\n\t\t\t\t\t204\t\t\t\tNo Content\t\t\t\t\t\t\tThis means you've made a successful DELETE to remove a hold or return a title\n\n\t\t\t\t\t400\t\t\t\tBad Request\t\t\t\t\t\t\tCannot process the request because it is malformed or incorrect.\n\t\t\t\t\t401\t\t\t\tUnauthorized\t\t\t\t\t\tRequired authentication information is either missing or not valid for the resource.\n\t\t\t\t\t403\t\t\t\tForbidden\t\t\t\t\t\t\tAccess is denied to the requested resource. The user might not have enough permission.\n\t\t\t\t\t404\t\t\t\tNot Found\t\t\t\t\t\t\tThe requested resource doesn’t exist.\n\t\t\t\t\t405\t\t\t\tMethod Not Allowed\t\t\t\t\tThe HTTP method in the request is not allowed on the resource.\n\t\t\t\t\t406\t\t\t\tNot Acceptable\t\t\t\t\t\tThis service doesn’t support the format requested in the Accept header.\n\t\t\t\t\t408\t\t\t\tRequest Time out\t\t\t\t\tCUSTOM ERROR - Not expected from OneDrive, but can be used to handle Internet connection failures the same (fallback and try again)\n\t\t\t\t\t409\t\t\t\tConflict\t\t\t\t\t\t\tThe current state conflicts with what the request expects. For example, the specified parent folder might not exist.\n\t\t\t\t\t410\t\t\t\tGone\t\t\t\t\t\t\t\tThe requested resource is no longer available at the server.\n\t\t\t\t\t411\t\t\t\tLength Required\t\t\t\t\t\tA Content-Length header is required on the request.\n\t\t\t\t\t412\t\t\t\tPrecondition Failed\t\t\t\t\tA precondition provided in the request (such as an if-match header) does not match the resource's current state.\n\t\t\t\t\t413\t\t\t\tRequest Entity Too Large\t\t\tThe request size exceeds the maximum limit.\n\t\t\t\t\t415\t\t\t\tUnsupported Media Type\t\t\t\tThe content type of the request is a format that is not supported by the service.\n\t\t\t\t\t416\t\t\t\tRequested Range Not Satisfiable\t\tThe specified byte range is invalid or unavailable.\n\t\t\t\t\t422\t\t\t\tUnprocessable Entity\t\t\t\tCannot process the request because it is semantically incorrect.\n\t\t\t\t\t423\t\t\t\tLocked\t\t\t\t\t\t\t\tThe file is currently checked out or locked for editing by another user\n\t\t\t\t\t429\t\t\t\tToo Many Requests\t\t\t\t\tClient application has been throttled and should not attempt to repeat the request until an amount of time has elapsed.\n\n\t\t\t\t\t500\t\t\t\tInternal Server Error\t\t\t\tThere was an internal server error while processing the request.\n\t\t\t\t\t501\t\t\t\tNot Implemented\t\t\t\t\t\tThe requested feature isn’t implemented.\n\t\t\t\t\t502\t\t\t\tBad Gateway\t\t\t\t\t\t\tThe service was unreachable\n\t\t\t\t\t503\t\t\t\tService Unavailable\t\t\t\t\tThe service is temporarily unavailable. You may repeat the request after a delay. There may be a Retry-After header.\n\t\t\t\t\t504\t\t\t\tGateway Timeout\t\t\t\t\t\tThe 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\n\t\t\t\t\t506\t\t\t\tVariant Also Negotiates\t\t\t\tCUSTOM ERROR - Received an unexpected response from Microsoft OneDrive\n\t\t\t\t\t507\t\t\t\tInsufficient Storage\t\t\t\tThe maximum storage quota has been reached.\n\t\t\t\t\t509\t\t\t\tBandwidth Limit Exceeded\t\t\tYour app has been throttled for exceeding the maximum bandwidth cap. Your app can retry the request again after more time has elapsed.\n\n\t\t\t\t\tHTTP/2 Response handling\n\n\t\t\t\t\t0\t\t\t\tOK\n\t\t\t\t\t\n\t\t\t\t**/\n\t\t\t\t\n\t\t\t\t// Detail the OneDriveAPI exception, debug output only\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"Handling a OneDrive API exception:\", [\"debug\"]);\n\t\t\t\t\taddLogEntry(to!string(response), [\"debug\"]);\n\t\t\t\t\t\n\t\t\t\t\t// Parse and display error message received from OneDrive\n\t\t\t\t\taddLogEntry(callingFunction ~ \"() - Generated a OneDriveException\", [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Perform action based on the HTTP Status Code\n\t\t\t\tswitch(exception.httpStatusCode) {\n\t\t\t\t\t\n\t\t\t\t\t//  0 - OK ... HTTP/2 version of 200 OK\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t//  100 - Continue\n\t\t\t\t\tcase 100:\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t\n\t\t\t\t\t//  408 - Request Time Out\n\t\t\t\t\t//  429 - Too Many Requests, backoff\n\t\t\t\t\tcase 408,429:\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\tif (exception.httpStatusCode == 408) {\n\t\t\t\t\t\t\taddLogEntry(\"Handling a Microsoft Graph API HTTP 408 Response Code (Request Time Out) - Internal Thread ID: \" ~ to!string(curlEngine.internalThreadId));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\taddLogEntry(\"Handling a Microsoft Graph API HTTP 429 Response Code (Too Many Requests) - Internal Thread ID: \" ~ to!string(curlEngine.internalThreadId));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Read in the Retry-After HTTP header as set and delay as per this value before retrying the request\n\t\t\t\t\t\tthisBackOffInterval = response.getRetryAfterValue();\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Using Retry-After Value = \" ~ to!string(thisBackOffInterval), [\"debug\"]);}\n\t\t\t\t\t\ttransientError = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t//  Transient errors\n\t\t\t\t\t//\t503 - Service Unavailable\n\t\t\t\t\t//  504 - Gateway Timeout\n\t\t\t\t\tcase 503,504:\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\tauto errorArray = splitLines(exception.msg);\n\t\t\t\t\t\taddLogEntry(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));\n\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t// Transient error - try again in 30 seconds\n\t\t\t\t\t\tthisBackOffInterval = 30;\n\t\t\t\t\t\ttransientError = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t// Default\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t// This exception should be then passed back to the original calling function for handling a OneDriveException\n\t\t\t\t\t\tthrow new OneDriveException(response.statusLine.code, response.statusLine.reason, response);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t// A FileSystem exception was thrown from somewhere\n\t\t\t} catch (FileException exception) {\n\t\t\t\t// There was a file system error - display the error message\n\t\t\t\tdisplayFileSystemErrorMessage(exception.msg, callingFunction, \"\"); // as we have no file path reference here, use a blank input\n\t\t\t\tthrow new OneDriveException(0, \"There was a file system error during OneDrive request: \" ~ exception.msg, response);\n\t\t\t\n\t\t\t// A OneDriveError was thrown\n\t\t\t} catch (OneDriveError exception) {\n\t\t\t\t// Disk space error or SSL error caused a OneDriveError to be thrown\n\t\t\t\t\n\t\t\t\t/**\n\t\t\t\t\n\t\t\t\tDO NOT UNCOMMENT THIS CODE UNLESS TESTING FOR THIS ISSUE: System SSL CA certificates are missing or unreadable by libcurl\n\t\t\t\t\n\t\t\t\t// Disk space error or SSL error\n\t\t\t\tif (getAvailableDiskSpace(\".\") == 0) {\n\t\t\t\t\t// Must exit\n\t\t\t\t\tforceExit();\n\t\t\t\t} else {\n\t\t\t\t\t// Catch the SSL error\n\t\t\t\t\taddLogEntry(\"WARNING: Disabling SSL peer verification due to libcurl failing to access the system CA certificate bundle (CAfile missing, unreadable, or misconfigured).\");\n\t\t\t\t\tsslVerifyPeerDisabled = true;\n\t\t\t\t\tcurlEngine.setDisableSSLVerifyPeer();\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t**/\n\t\t\t\t\n\t\t\t\t// Must exit\n\t\t\t\tforceExit();\n\t\t\t}\n\n\t\t\t// Increment re-try counter\n\t\t\tretryAttempts++;\n\n\t\t\t// Configure libcurl to perform a fresh connection on API retry\n\t\t\tsetFreshConnectOption();\n\t\t\t\n\t\t\t// Has maxRetryCount been reached?\n\t\t\tif (retryAttempts > maxRetryCount) {\n\t\t\t\taddLogEntry(\"ERROR: Unable to reconnect to the Microsoft OneDrive service after \" ~ to!string(retryAttempts) ~ \" attempts lasting approximately 365 days\");\n\t\t\t\tthrow new OneDriveException(408, \"Request Timeout - HTTP 408 or Internet down?\", response);\n\t\t\t} else {\n\t\t\t\t// Was 'thisBackOffInterval' set by a 429 event ?\n\t\t\t\tif (thisBackOffInterval == 0) {\n\t\t\t\t\t// Calculate and apply exponential backoff upto a maximum of 120 seconds before the API call is re-tried\n\t\t\t\t\tthisBackOffInterval = calculateBackoff(retryAttempts, baseBackoffInterval, maxBackoffInterval);\n\t\t\t\t\t// If this 'somehow' calculates a negative number, this is not correct .. and this has been seen in testing - unknown cause\n\t\t\t\t\t// \n\t\t\t\t\t// Retry attempt:           31 - Internal Thread ID: ICO4ELBlGXFwyTzh\n\t\t\t\t\t//  This attempt timestamp: 2024-Aug-10 10:32:07\n\t\t\t\t\t//  Next retry in approx:   -2147483648 seconds\n\t\t\t\t\t//  Next retry approx:      1956-Jul-23 07:17:59\n\t\t\t\t\t// Illegal instruction (core dumped)\n\t\t\t\t\t// \n\t\t\t\t\t// Set to 'maxBackoffInterval' if calculated value is negative\n\t\t\t\t\tif (thisBackOffInterval < 0) {\n\t\t\t\t\t\tthisBackOffInterval = maxBackoffInterval;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// set the current time for this thread\n\t\t\t\tcurrentTime = Clock.currTime();\n\t\t\t\tcurrentTime.fracSecs = Duration.zero;\n\t\t\t\t\n\t\t\t\t// If verbose logging, detail when we are re-trying the call\n\t\t\t\tif (verboseLogging) {\n\t\t\t\t\tauto timeString = currentTime.toString();\n\t\t\t\t\taddLogEntry(\"Retry attempt:           \" ~ to!string(retryAttempts) ~ \" - Internal Thread ID: \" ~ to!string(curlEngine.internalThreadId), [\"verbose\"]);\n\t\t\t\t\taddLogEntry(\" This attempt timestamp: \" ~ timeString, [\"verbose\"]);\n\t\t\t\t\t// Detail when the next attempt will be tried\n\t\t\t\t\t// Factor in the delay for curl to generate the exception - otherwise the next timestamp appears to be 'out' even though technically correct\n\t\t\t\t\tauto nextRetry = currentTime + dur!\"seconds\"(thisBackOffInterval) + dur!\"seconds\"(timestampAlign);\n\t\t\t\t\taddLogEntry(\" Next retry in approx:   \" ~ to!string((thisBackOffInterval + timestampAlign)) ~ \" seconds\");\n\t\t\t\t\taddLogEntry(\" Next retry approx:      \" ~ to!string(nextRetry), [\"verbose\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Thread sleep\n\t\t\t\tThread.sleep(dur!\"seconds\"(thisBackOffInterval));\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Reset SSL Peer Validation if it was disabled\n\t\tif (sslVerifyPeerDisabled) {\n\t\t\tcurlEngine.setEnableSSLVerifyPeer();\n\t\t}\n\t\t\n\t\t// Return the result\n\t\treturn result;\n\t}\n\t\n\t// Check the HTTP Response code and determine if a OneDriveException should be thrown\n\tprivate bool checkHttpResponseCode(int httpResponseCode) {\n\t\n\t\tbool shouldThrow = false;\n\t\t\n\t\t// Redirect Codes\n\t\timmutable acceptedRedirectCodes = [301, 302, 304, 307, 308];\n\n\t\t//\n\t\t// This condition checks if the HTTP response code falls within the acceptable range for both HTTP 1.1 and HTTP 2.0.\n\t\t//\n\t\t// For HTTP 1.1:\n\t\t// - Any 1xx response (Informational responses, ranging from 100 to 199)\n\t\t// - Any 2xx response (Successful responses, ranging from 200 to 299)\n\t\t// - A 302 response (Temporary Redirect)\n\t\t//\n\t\t// For HTTP 2.0:\n\t\t// - Any 1xx response (Informational responses, ranging from 100 to 199)\n\t\t// - Any 2xx response (Successful responses, ranging from 200 to 299)\n\t\t// - A 302 response (Temporary Redirect)\n\t\t// - A 0 response (Interpreted as 200 OK based on empirical evidence)\n\t\t//\n\t\t// If the HTTP response code meets any of these conditions, it is considered acceptable, and no exception will be thrown.\n\t\t//\n\t\t\n\t\tif ((httpResponseCode >= 100 && httpResponseCode < 300) || canFind(acceptedRedirectCodes, httpResponseCode) || httpResponseCode == 0) {\n            shouldThrow = false;\n        } else {\n            shouldThrow = true;\n        }\n\t\n\t\t// return evaluation\n\t\treturn shouldThrow;\n\t}\n\t\n\t// Calculates the delay for exponential backoff\n\tprivate int calculateBackoff(int retryAttempts, int baseInterval, int maxInterval) {\n\t\tint cappedAttempts = min(retryAttempts, 10); // Prevent exponent overflow\n\t\tint backoff = baseInterval * (1 << cappedAttempts);\n\t\treturn min(backoff, maxInterval);\n\t}\n\t\n\t// Configure libcurl to perform a fresh connection\n\tprivate void setFreshConnectOption() {\n\t\tif (debugLogging) {addLogEntry(\"Configuring libcurl to use a fresh connection for re-try\", [\"debug\"]);}\n\t\tcurlEngine.http.handle.set(CurlOption.fresh_connect,1);\n\t\t// Set libcurl dns_cache_timeout timeout\n\t\t// https://curl.se/libcurl/c/CURLOPT_DNS_CACHE_TIMEOUT.html\n\t\t// https://dlang.org/library/std/net/curl/http.dns_timeout.html\n\t\tcurlEngine.http.dnsTimeout = (dur!\"seconds\"(0));\n\t}\n\t\n\t// Unset the libcurl fresh connection options and reset libcurl DNS Cache Timeout\n\tprivate void unsetFreshConnectOption() {\n\t\tif (debugLogging) {addLogEntry(\"Unsetting libcurl to use a fresh connection as this causes a performance impact if left enabled\", [\"debug\"]);}\n\t\tcurlEngine.http.handle.set(CurlOption.fresh_connect,0);\n\t\t// Reset libcurl dns_cache_timeout timeout\n\t\t// https://curl.se/libcurl/c/CURLOPT_DNS_CACHE_TIMEOUT.html\n\t\t// https://dlang.org/library/std/net/curl/http.dns_timeout.html\n\t\tcurlEngine.http.dnsTimeout = (dur!\"seconds\"(appConfig.getValueLong(\"dns_timeout\")));\n\t}\n\t\n\t// Generate a HTTP 'reason' based on the HTTP 'code'\n\tprivate string getMicrosoftGraphStatusMessage(ushort code) {\n\t\tstring message;\n\t\tswitch (code) {\n\t\t\tcase 200:\n\t\t\t\tmessage = \"OK\";\n\t\t\t\tbreak;\n\t\t\tcase 201:\n\t\t\t\tmessage = \"Created\";\n\t\t\t\tbreak;\n\t\t\tcase 202:\n\t\t\t\tmessage = \"Accepted\";\n\t\t\t\tbreak;\n\t\t\tcase 204:\n\t\t\t\tmessage = \"No Content\";\n\t\t\t\tbreak;\n\t\t\tcase 301:\n\t\t\t\tmessage = \"Moved Permanently\";\n\t\t\t\tbreak;\n\t\t\tcase 302:\n\t\t\t\tmessage = \"Found\";\n\t\t\t\tbreak;\n\t\t\tcase 304:\n\t\t\t\tmessage = \"Not Modified\";\n\t\t\t\tbreak;\n\t\t\tcase 307:\n\t\t\t\tmessage = \"Temporary Redirect\";\n\t\t\t\tbreak;\n\t\t\tcase 308:\n\t\t\t\tmessage = \"Permanent Redirect\";\n\t\t\t\tbreak;\n\t\t\tcase 400:\n\t\t\t\tmessage = \"Bad Request\";\n\t\t\t\tbreak;\n\t\t\tcase 401:\n\t\t\t\tmessage = \"Unauthorized\";\n\t\t\t\tbreak;\n\t\t\tcase 402:\n\t\t\t\tmessage = \"Payment Required\";\n\t\t\t\tbreak;\n\t\t\tcase 403:\n\t\t\t\tmessage = \"Forbidden\";\n\t\t\t\tbreak;\n\t\t\tcase 404:\n\t\t\t\tmessage = \"Not Found\";\n\t\t\t\tbreak;\n\t\t\tcase 405:\n\t\t\t\tmessage = \"Method Not Allowed\";\n\t\t\t\tbreak;\n\t\t\tcase 406:\n\t\t\t\tmessage = \"Not Acceptable\";\n\t\t\t\tbreak;\n\t\t\tcase 409:\n\t\t\t\tmessage = \"Conflict\";\n\t\t\t\tbreak;\n\t\t\tcase 410:\n\t\t\t\tmessage = \"Gone\";\n\t\t\t\tbreak;\n\t\t\tcase 411:\n\t\t\t\tmessage = \"Length Required\";\n\t\t\t\tbreak;\n\t\t\tcase 412:\n\t\t\t\tmessage = \"Precondition Failed\";\n\t\t\t\tbreak;\n\t\t\tcase 413:\n\t\t\t\tmessage = \"Request Entity Too Large\";\n\t\t\t\tbreak;\n\t\t\tcase 415:\n\t\t\t\tmessage = \"Unsupported Media Type\";\n\t\t\t\tbreak;\n\t\t\tcase 416:\n\t\t\t\tmessage = \"Requested Range Not Satisfiable\";\n\t\t\t\tbreak;\n\t\t\tcase 422:\n\t\t\t\tmessage = \"Unprocessable Entity\";\n\t\t\t\tbreak;\n\t\t\tcase 423:\n\t\t\t\tmessage = \"Locked\";\n\t\t\t\tbreak;\n\t\t\tcase 429:\n\t\t\t\tmessage = \"Too Many Requests\";\n\t\t\t\tbreak;\n\t\t\tcase 500:\n\t\t\t\tmessage = \"Internal Server Error\";\n\t\t\t\tbreak;\n\t\t\tcase 501:\n\t\t\t\tmessage = \"Not Implemented\";\n\t\t\t\tbreak;\n\t\t\tcase 503:\n\t\t\t\tmessage = \"Service Unavailable\";\n\t\t\t\tbreak;\n\t\t\tcase 504:\n\t\t\t\tmessage = \"Gateway Timeout\";\n\t\t\t\tbreak;\n\t\t\tcase 507:\n\t\t\t\tmessage = \"Insufficient Storage\";\n\t\t\t\tbreak;\n\t\t\tcase 509:\n\t\t\t\tmessage = \"Bandwidth Limit Exceeded\";\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tmessage = \"Unknown Status Code\";\n\t\t\t\tbreak;\n\t\t}\n\t\treturn message;\n\t}\n}"
  },
  {
    "path": "src/qxor.d",
    "content": "// What is this module called?\nmodule qxor;\n\n// What does this module require to function?\nimport std.algorithm;\nimport std.digest;\n\n// Implementation of the QuickXorHash algorithm in D\n// https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/code-snippets/quickxorhash.md\nstruct QuickXor {\n\tprivate enum int widthInBits = 160;\n\tprivate enum size_t lengthInBytes = (widthInBits - 1) / 8 + 1;\n\tprivate enum size_t lengthInQWords = (widthInBits - 1) / 64 + 1;\n\tprivate enum int bitsInLastCell = widthInBits % 64; // 32\n\tprivate enum int shift = 11;\n\n\tprivate ulong[lengthInQWords] _data;\n\tprivate ulong _lengthSoFar;\n\tprivate int _shiftSoFar;\n\n\tnothrow @safe void put(scope const(ubyte)[] array...) {\n\t\tint vectorArrayIndex = _shiftSoFar / 64;\n\t\tint vectorOffset = _shiftSoFar % 64;\n\t\timmutable size_t iterations = min(array.length, widthInBits);\n\n\t\tfor (size_t i = 0; i < iterations; i++) {\n\t\t\timmutable bool isLastCell = vectorArrayIndex == _data.length - 1;\n\t\t\timmutable int bitsInVectorCell = isLastCell ? bitsInLastCell : 64;\n\n\t\t\tif (vectorOffset <= bitsInVectorCell - 8) {\n\t\t\t\t for (size_t j = i; j < array.length; j += widthInBits) {\n\t\t\t\t\t_data[vectorArrayIndex] ^= cast(ulong) array[j] << vectorOffset;\n\t\t\t\t }\n\t\t\t} else {\n\t\t\t\tint index1 = vectorArrayIndex;\n\t\t\t\tint index2 = isLastCell ? 0 : (vectorArrayIndex + 1);\n\t\t\t\tubyte low = cast(ubyte) (bitsInVectorCell - vectorOffset);\n\n\t\t\t\tubyte xoredByte = 0;\n\t\t\t\tfor (size_t j = i; j < array.length; j += widthInBits) {\n\t\t\t\t\txoredByte ^= array[j];\n\t\t\t\t}\n\n\t\t\t\t_data[index1] ^= cast(ulong) xoredByte << vectorOffset;\n\t\t\t\t_data[index2] ^= cast(ulong) xoredByte >> low;\n\t\t\t}\n\n\t\t\tvectorOffset += shift;\n\t\t\tif (vectorOffset >= bitsInVectorCell) {\n\t\t\t\tvectorArrayIndex = isLastCell ? 0 : vectorArrayIndex + 1;\n\t\t\t\tvectorOffset -= bitsInVectorCell;\n\t\t\t}\n\t\t}\n\n\t\t_shiftSoFar = cast(int) (_shiftSoFar + shift * (array.length % widthInBits)) % widthInBits;\n\t\t_lengthSoFar += array.length;\n\t}\n\n\tnothrow @safe void start() {\n\t\t_data = _data.init;\n\t\t_shiftSoFar = 0;\n\t\t_lengthSoFar = 0;\n\t}\n\n\tnothrow @trusted ubyte[lengthInBytes] finish() {\n\t\tubyte[lengthInBytes] tmp;\n\t\ttmp[0 .. lengthInBytes] = (cast(ubyte*) _data)[0 .. lengthInBytes];\n\t\tfor (size_t i = 0; i < 8; i++) {\n\t\t\ttmp[lengthInBytes - 8 + i] ^= (cast(ubyte*) &_lengthSoFar)[i];\n        }\n\t\treturn tmp;\n\t}\n}"
  },
  {
    "path": "src/socketio.d",
    "content": "// What is this module called?\nmodule socketio;\n\n// What does this module require to function?\nimport core.atomic     : atomicLoad, atomicStore;\nimport core.thread     : Thread;\nimport core.time       : Duration, dur;\nimport std.concurrency : spawn, Tid, thisTid, send, receiveTimeout;\nimport std.conv        : to;\nimport std.datetime    : SysTime, Clock, UTC;\nimport std.exception   : collectException;\nimport std.json        : JSONValue, JSONType, parseJSON;\nimport std.net.curl    : CurlException;\nimport std.socket      : SocketException;\nimport std.string      : indexOf;\n\n// What other modules that we have created do we need to import?\nimport log;\nimport util;\nimport config;\nimport curlWebsockets;\n\n// ========== Logging Shim ==========\nprivate void logSocketIOOutput(string s) {\n\tif (debugLogging) {\n\t\taddLogEntry(\"SOCKETIO: \" ~ s, [\"debug\"]);\n\t}\n}\n\nfinal class OneDriveSocketIo {\n\tprivate Tid parentTid;\n\tprivate ApplicationConfig appConfig;\n\tprivate bool started = false;\n\tprivate Duration renewEarly = dur!\"seconds\"(120);\n\tprivate string engineSid;\n\tprivate bool expiryWarned = false;\n\tprivate bool renewRequested = false;\n\tprivate string currentNotifUrl;\n\n\t// Worker / state\n\tprivate Tid controllerTid; // main/control thread to notify when the worker exits\n\tprivate Tid workerTid;\n\tprivate shared bool pleaseStop = false;\n\tprivate long  pingIntervalMs = 25000;\n\tprivate long  pingTimeoutMs  = 60000;\n\tprivate bool  namespaceOpened = false;\n\tprivate CurlWebSocket ws;\n\tprivate shared bool workerExited = false; // set true by run() on clean exit\n\npublic:\n\tthis(Tid parentTid, ApplicationConfig appConfig) {\n\t\tthis.parentTid = parentTid;\n\t\tthis.appConfig = appConfig;\n\t}\n\t\n\t~this() {\n\t\tlogSocketIOOutput(\"Signalling to stop a OneDriveSocketIo instance\");\n\t\tstop(); // sets pleaseStop + waits for workerExited\n\n\t\tif (atomicLoad(workerExited)) {\n\t\t\tif (ws !is null) {\n\t\t\t\tlogSocketIOOutput(\"Attempting to destroy libcurl RFC6455 WebSocket client cleanly\");\n\t\t\t\t// Worker has exited; safe to close/cleanup/destroy\n\t\t\t\tcollectException(ws.close(1000, \"client stop\"));\n\t\t\t\tcollectException(ws.cleanupCurlHandle());\n\t\t\t\tlogSocketIOOutput(\"Cleaned up an instance of a CurlWebSocket object via cleanupCurlHandle()\");\n\t\t\t\tobject.destroy(ws);\n\t\t\t\tws = null;\n\t\t\t\tlogSocketIOOutput(\"Destroyed libcurl RFC6455 WebSocket client cleanly\");\n\t\t\t}\n\t\t} else {\n\t\t\t// Worker still running; DO NOT touch ws/curl from this thread.\n\t\t\tlogSocketIOOutput(\"Worker still running; skipping ws destruction to avoid race.\");\n\t\t}\n\t}\n\t\n\tvoid start() {\n\t\tif (started) return;\n\t\t// Get current WebSocket Notification URL\n\t\tcurrentNotifUrl = appConfig.websocketNotificationUrl;\n\t\t\n\t\t// Reset cooperative flags\n\t\tpleaseStop = false;\n\t\tatomicStore(workerExited, false);\n\t\t\n\t\t// Set Flag\n\t\tstarted = true;\n\t\t\n\t\t// Spawn worker thread\n\t\tworkerTid = spawn(&run, cast(shared) this);\n\t}\n\n\tvoid stop() {\n\t\tif (!started) return;\n\n\t\t// Ask the worker to stop cooperatively\n\t\tpleaseStop = true;\n\t\tlogSocketIOOutput(\"Flagged to stop WebSocket monitoring of Microsoft Graph API changes.\");\n\t\t// Wait up to ~6 seconds for the worker to finish cleanup.\n\t\t// No mailbox usage here to avoid nested receiveTimeout on FreeBSD.\n\t\tenum int totalWaitMs = 6000;\n\t\tenum int stepMs = 100;\n\t\tint waited = 0;\n\t\t\n\t\twhile (!atomicLoad(workerExited) && waited < totalWaitMs) {\n\t\t\tThread.sleep(dur!\"msecs\"(stepMs));\n\t\t\twaited += stepMs;\n\t\t}\n\t\t\n\t\t// Mark not started only after we know we've requested stop\n\t\tstarted = false;\n\n\t\tif (!atomicLoad(workerExited)) {\n\t\t\t// We asked nicely but didn’t get an ack within the window; continue shutdown anyway.\n\t\t\t// Keeps behaviour safe; avoids hanging the main shutdown path\n\t\t\tlogSocketIOOutput(\"Worker stop acknowledgement not received within timeout; continuing shutdown.\");\n\t\t}\n\t}\n\n\tDuration getNextExpirationCheckDuration() {\n\t\tif (appConfig.websocketUrlExpiry.length == 0)\n\t\t\treturn dur!\"seconds\"(5);\n\n\t\tSysTime expiry;\n\t\tauto err = collectException(expiry = SysTime.fromISOExtString(appConfig.websocketUrlExpiry));\n\t\tif (err !is null)\n\t\t\treturn dur!\"seconds\"(5);\n\n\t\tauto now = Clock.currTime(UTC());\n\t\tif (expiry <= now) return dur!\"seconds\"(5);\n\n\t\tauto delta = expiry - now;\n\t\tif (delta > renewEarly) delta -= renewEarly;\n\t\treturn (delta > Duration.zero) ? delta : dur!\"seconds\"(5);\n\t}\n\nprivate:\n\t// Main function that listens and sends events\n\tstatic void run(shared OneDriveSocketIo _this) {\n\t\tlogSocketIOOutput(\"run() entered\");\n\n\t\tauto self = cast(OneDriveSocketIo) _this;\n\n\t\t// Capped exponential backoff: 1s, 2s, 4s, ... up to 60s\n\t\tint backoffSeconds = 1;\n\t\tconst int maxBackoffSeconds = 60;\n\t\tbool online;\n\t\t\n\t\tscope(exit) {\n\t\t\t// Signal that the worker is fully done (visible across threads)\n\t\t\tatomicStore(self.workerExited, true);\n\t\t\t\n\t\t\t// Log that we are exiting the run() function\n\t\t\tlogSocketIOOutput(\"run() exiting\");\n\t\t}\n\n\t\twhile (!self.pleaseStop) {\n\t\t\t// Catch network exceptions at the socketio-loop level and treat them as recoverable\n\t\t\ttry {\n\t\t\t\t// If we're offline (or OneDrive service not reachable), don't bother trying yet\n\t\t\t\tlogSocketIOOutput(\"Testing network to ensure network connectivity to Microsoft OneDrive Service\");\n\t\t\t\tonline = testInternetReachability(self.appConfig, false); // Will display failures, but nothing if successful .. a quiet check of sorts.\n\t\t\t\tif (!online) {\n\t\t\t\t\tlogSocketIOOutput(\"Network or OneDrive service not reachable; delaying reconnect\");\n\t\t\t\t\tlogSocketIOOutput(\"Backoff \" ~ to!string(backoffSeconds) ~ \"s before retry\");\n\t\t\t\t\tThread.sleep(dur!\"seconds\"(backoffSeconds));\n\t\t\t\t\tif (backoffSeconds < maxBackoffSeconds) backoffSeconds *= 2;\n\t\t\t\t\tcontinue;\n\t\t\t\t} else {\n\t\t\t\t\t// We are 'online'\n\t\t\t\t\t// Build Socket.IO WS URL from notificationUrl\n\t\t\t\t\tstring notif = self.appConfig.websocketNotificationUrl;\n\t\t\t\t\tif (notif.length == 0) {\n\t\t\t\t\t\tlogSocketIOOutput(\"No notificationUrl available; will retry\");\n\t\t\t\t\t\tlogSocketIOOutput(\"Backoff \" ~ to!string(backoffSeconds) ~ \"s before retry\");\n\t\t\t\t\t\tThread.sleep(dur!\"seconds\"(backoffSeconds));\n\t\t\t\t\t\tif (backoffSeconds < maxBackoffSeconds) backoffSeconds *= 2;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tself.currentNotifUrl = notif;\n\t\t\t\t\tstring wsUrl = toSocketIoWsUrl(notif);\n\n\t\t\t\t\t// Fresh WS instance per attempt\n\t\t\t\t\tself.ws = new CurlWebSocket();\n\n\t\t\t\t\t// Use application configuration values\n\t\t\t\t\tself.ws.setUserAgent(self.appConfig.getValueString(\"user_agent\"));\n\t\t\t\t\tself.ws.setHTTPSDebug(self.appConfig.getValueBool(\"debug_https\"));\n\t\t\t\t\tself.ws.setTimeouts(10000, 15000);\n\n\t\t\t\t\t// Connect to Microsoft Graph API using WebSockets and Socket.IO v4\n\t\t\t\t\tlogSocketIOOutput(\"Connecting to \" ~ wsUrl);\n\t\t\t\t\tauto rc = self.ws.connect(wsUrl);\n\t\t\t\t\tif (rc != 0) {\n\t\t\t\t\t\tlogSocketIOOutput(\"self.ws.connect failed; will retry\");\n\t\t\t\t\t\tcollectException(self.ws.close(1002, \"connect-failed\"));\n\t\t\t\t\t\tThread.sleep(dur!\"seconds\"(backoffSeconds));\n\t\t\t\t\t\tif (backoffSeconds < maxBackoffSeconds) backoffSeconds *= 2;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Socket.IO handshake: wait for '0{json}'\n\t\t\t\t\tif (!awaitEngineOpen(self.ws, self)) {\n\t\t\t\t\t\tlogSocketIOOutput(\"Socket.IO open handshake failed; will retry\");\n\t\t\t\t\t\tcollectException(self.ws.close(1002, \"handshake-failed\"));\n\t\t\t\t\t\tThread.sleep(dur!\"seconds\"(backoffSeconds));\n\t\t\t\t\t\tif (backoffSeconds < maxBackoffSeconds) backoffSeconds *= 2;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Open default namespace: send \"40\"\n\t\t\t\t\tlogSocketIOOutput(\"Sending Socket.IO connect (40) to default namespace\");\n\t\t\t\t\tif (self.ws.sendText(\"40\") != 0) {\n\t\t\t\t\t\tlogSocketIOOutput(\"Failed to send 40 (open namespace); will retry\");\n\t\t\t\t\t\tcollectException(self.ws.close(1002, \"ns40-failed\"));\n\t\t\t\t\t\tThread.sleep(dur!\"seconds\"(backoffSeconds));\n\t\t\t\t\t\tif (backoffSeconds < maxBackoffSeconds) backoffSeconds *= 2;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogSocketIOOutput(\"Sent Socket.IO connect '40' for namespace '/'\");\n\t\t\t\t\t}\n\n\t\t\t\t\t// Open 'notifications' namespace: send \"40/notifications\"\n\t\t\t\t\tlogSocketIOOutput(\"Sending Socket.IO connect (40) to '/notifications' namespace\");\n\t\t\t\t\tif (self.ws.sendText(\"40/notifications\") != 0) {\n\t\t\t\t\t\tlogSocketIOOutput(\"Failed to send 40 for '/notifications' namespace; will retry\");\n\t\t\t\t\t\tcollectException(self.ws.close(1002, \"ns40-failed\"));\n\t\t\t\t\t\tThread.sleep(dur!\"seconds\"(backoffSeconds));\n\t\t\t\t\t\tif (backoffSeconds < maxBackoffSeconds) backoffSeconds *= 2;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogSocketIOOutput(\"Sent Socket.IO connect '40' for namespace '/notifications'\");\n\t\t\t\t\t}\n\n\t\t\t\t\t// Connected successfully → reset backoff\n\t\t\t\t\tbackoffSeconds = 1;\n\t\t\t\t\t// Reset per-connection flags so renew logic and ns-open tracking work after reconnection\n\t\t\t\t\tself.expiryWarned   = false;\n\t\t\t\t\tself.renewRequested = false;\n\t\t\t\t\tself.namespaceOpened = false;\n\n\t\t\t\t\t// Track last server ping received to detect a dead connection\n\t\t\t\t\tSysTime lastPingAt = Clock.currTime(UTC());\n\n\t\t\t\t\t// Listen for Socket.IO Events\n\t\t\t\t\tfor (;;) {\n\t\t\t\t\t\t// Stop request\n\t\t\t\t\t\tif (self.pleaseStop) {\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Stop requested; shutting down run() loop\");\n\t\t\t\t\t\t\tcollectException(self.ws.close(1000, \"stop-requested\"));\n\t\t\t\t\t\t\tcollectException(self.ws.cleanupCurlHandle());\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Cleaned up an instance of a CurlWebSocket object via cleanupCurlHandle()\");\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Subscription nearing expiry? (informational; renewal happens elsewhere)\n\t\t\t\t\t\tif (!self.expiryWarned && self.appConfig.websocketUrlExpiry.length > 0) {\n\t\t\t\t\t\t\tSysTime expiry;\n\t\t\t\t\t\t\tauto e = collectException(expiry = SysTime.fromISOExtString(self.appConfig.websocketUrlExpiry));\n\t\t\t\t\t\t\tif (e is null) {\n\t\t\t\t\t\t\t\tauto remain = expiry - Clock.currTime(UTC());\n\t\t\t\t\t\t\t\tif (remain <= dur!\"minutes\"(5)) {\n\t\t\t\t\t\t\t\t\tself.expiryWarned = true; // emit only once\n\t\t\t\t\t\t\t\t\tlogSocketIOOutput(\"subscription nearing expiry; renewal required soon\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Renewal window check (emit once; 2 minutes before)\n\t\t\t\t\t\tif (!self.renewRequested && self.appConfig.websocketUrlExpiry.length > 0) {\n\t\t\t\t\t\t\tSysTime expiry;\n\t\t\t\t\t\t\tauto e = collectException(expiry = SysTime.fromISOExtString(self.appConfig.websocketUrlExpiry));\n\t\t\t\t\t\t\tif (e is null) {\n\t\t\t\t\t\t\t\tauto remain = expiry - Clock.currTime(UTC());\n\t\t\t\t\t\t\t\tif (remain <= dur!\"minutes\"(2)) {\n\t\t\t\t\t\t\t\t\tself.renewRequested = true;\n\t\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Subscription nearing expiry; requesting renewal from main() monitor loop\");\n\t\t\t\t\t\t\t\t\tsend(self.parentTid, \"SOCKETIO_RENEWAL_REQUEST\");\n\t\t\t\t\t\t\t\t\tsend(self.parentTid, \"SOCKETIO_RENEWAL_CONTEXT:\" ~ \"id=\" ~ self.appConfig.websocketEndpointResponse ~ \" url=\" ~ self.appConfig.websocketNotificationUrl);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// If we haven't seen a server ping within pingInterval + pingTimeout → treat as dead link\n\t\t\t\t\t\tauto now = Clock.currTime(UTC());\n\t\t\t\t\t\tauto maxSilence = dur!\"msecs\"(self.pingIntervalMs + self.pingTimeoutMs);\n\t\t\t\t\t\tif (now - lastPingAt > maxSilence) {\n\t\t\t\t\t\t\tlogSocketIOOutput(\"No server ping within expected window; restarting WebSocket\");\n\t\t\t\t\t\t\tbreak; // fall out to backoff/retry\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Reconnect to a new endpoint if main updated websocketNotificationUrl\n\t\t\t\t\t\tif (self.appConfig.websocketNotificationUrl.length > 0 &&\n\t\t\t\t\t\t\tself.appConfig.websocketNotificationUrl != self.currentNotifUrl) {\n\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Detected new notificationUrl; reconnecting\");\n\t\t\t\t\t\t\tcollectException(self.ws.close(1000, \"reconnect\"));\n\t\t\t\t\t\t\tcollectException(self.ws.cleanupCurlHandle());\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Cleaned up an instance of a CurlWebSocket object via cleanupCurlHandle()\");\n\n\t\t\t\t\t\t\t// Establish a fresh connection and handshakes\n\t\t\t\t\t\t\tself.currentNotifUrl = self.appConfig.websocketNotificationUrl;\n\t\t\t\t\t\t\tstring newWsUrl = toSocketIoWsUrl(self.currentNotifUrl);\n\t\t\t\t\t\t\tself.ws = new CurlWebSocket();\n\t\t\t\t\t\t\tself.ws.setUserAgent(self.appConfig.getValueString(\"user_agent\"));\n\t\t\t\t\t\t\tself.ws.setTimeouts(10000, 15000);\n\t\t\t\t\t\t\tself.ws.setHTTPSDebug(self.appConfig.getValueBool(\"debug_https\"));\n\n\t\t\t\t\t\t\tauto rc2 = self.ws.connect(newWsUrl);\n\t\t\t\t\t\t\tif (rc2 != 0) {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"reconnect failed\");\n\t\t\t\t\t\t\t\tbreak; // fall out to backoff/retry\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!awaitEngineOpen(self.ws, self)) {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Socket.IO open after reconnect failed\");\n\t\t\t\t\t\t\t\tbreak; // fall out to backoff/retry\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Open default namespace again\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Sending Socket.IO connect (40) to default namespace\");\n\t\t\t\t\t\t\tif (self.ws.sendText(\"40\") != 0) {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Failed to send 40 (open namespace)\");\n\t\t\t\t\t\t\t\tbreak; // fall out to backoff/retry\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Sent Socket.IO connect '40' for namespace '/'\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Open '/notifications' again (best-effort)\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Sending Socket.IO connect (40) to '/notifications' namespace\");\n\t\t\t\t\t\t\tif (self.ws.sendText(\"40/notifications\") != 0) {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Failed to send 40 for '/notifications' namespace\");\n\t\t\t\t\t\t\t\tbreak; // fall out to backoff/retry\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Sent Socket.IO connect '40' for namespace '/notifications'\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Reset ping reference after a clean reconnect\n\t\t\t\t\t\t\tlastPingAt = Clock.currTime(UTC());\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Receive message\n\t\t\t\t\t\tauto msg = self.ws.recvText();\n\t\t\t\t\t\tif (msg.length == 0) {\n\t\t\t\t\t\t\tThread.sleep(dur!\"msecs\"(20));\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Socket.IO parsing\n\t\t\t\t\t\tif (msg.length > 0 && msg[0] == '2') {\n\t\t\t\t\t\t\t// Server ping -> immediate pong, and mark last ping time\n\t\t\t\t\t\t\tif (self.ws.sendText(\"3\") != 0) {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Failed sending Socket.IO pong '3'\");\n\t\t\t\t\t\t\t\tbreak; // fall out to backoff/retry\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlastPingAt = Clock.currTime(UTC());\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Socket.IO ping received, → pong sent\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (msg.length > 0 && msg[0] == '3') {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t} else if (msg.length > 1 && msg[0] == '4' && msg[1] == '2') {\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Received 42 msg = \" ~ to!string(msg));\n\t\t\t\t\t\t\thandleSocketIoEvent(msg, self);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t} else if (msg.length > 1 && msg[0] == '4' && msg[1] == '0') {\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Received 40 msg = \" ~ to!string(msg));\n\t\t\t\t\t\t\t// 40{\"sid\":...} or 40/notifications,{...}\n\t\t\t\t\t\t\tsize_t i = 3;\n\t\t\t\t\t\t\twhile (i < msg.length && msg[i] != ',') i++;\n\t\t\t\t\t\t\tauto ns = msg[3 .. i];\n\n\t\t\t\t\t\t\tif (ns == \"notifications\") {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Namespace '/notifications' opened; listening for Socket.IO events via WebSocket Transport\");\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlogSocketIOOutput(\"Namespace '/' opened; listening for Socket.IO events via WebSocket Transport\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tself.namespaceOpened = true;\n\t\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t\t} else if (msg.length > 1 && msg[0] == '4' && msg[1] == '1') {\n\t\t\t\t\t\t\tlogSocketIOOutput(\"got 41 (disconnect)\");\n\t\t\t\t\t\t\tbreak; // fall out to backoff/retry\n\t\t\t\t\t\t} else if (msg.length > 0 && msg[0] == '0') {\n\t\t\t\t\t\t\tparseEngineOpenFromPacket(msg, self);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlogSocketIOOutput(\"Received Unhandled Message: \" ~ msg);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fell out of the inner loop → close and backoff, then retry\n\t\t\t\t\tlogSocketIOOutput(\"Retrying WebSocket Connection\");\n\t\t\t\t\tcollectException(self.ws.close(1001, \"reconnect\"));\n\t\t\t\t\tlogSocketIOOutput(\"Backoff \" ~ to!string(backoffSeconds) ~ \"s before retry\");\n\t\t\t\t\tThread.sleep(dur!\"seconds\"(backoffSeconds));\n\t\t\t\t\tif (backoffSeconds < maxBackoffSeconds) backoffSeconds *= 2;\n\t\t\t\t}\n\t\t\t} catch (CurlException e) {\n\t\t\t\t// Caught a CurlException\n\t\t\t\taddLogEntry(\"Network error during socketio loop: \" ~ e.msg ~ \" (will retry)\");\n\t\t\t\tThread.sleep(dur!\"seconds\"(5));\n\t\t\t} catch (SocketException e) {\n\t\t\t\t// Caught a SocketException\n\t\t\t\taddLogEntry(\"Socket error during socketio loop: \" ~ e.msg ~ \" (will retry)\");\n\t\t\t\tThread.sleep(dur!\"seconds\"(5));\n\t\t\t} catch (Exception e) {\n\t\t\t\t// Caught some other error\n\t\t\t\taddLogEntry(\"Unexpected error during socketio loop: \" ~ e.toString());\n\t\t\t\tThread.sleep(dur!\"seconds\"(5));\n\t\t\t}\n\t\t}\n\t}\n\n\t// Convert the notificationURL into a usable WebSocket URL\n\tstatic string toSocketIoWsUrl(string notificationUrl) {\n\t\t// input:  https://host/notifications?token=...&applicationId=...\n\t\t// output: wss://host/socket.io/?EIO=4&transport=websocket&token=...&applicationId=...\n\t\tlogSocketIOOutput(\"toSocketIoWsUrl input: \" ~ notificationUrl);\n\t\t\n\t\tsize_t schemePos = notificationUrl.length;\n\t\t{\n\t\t\tauto pos = cast(ptrdiff_t) -1;\n\t\t\t// manual indexOf(\"://\") without std.string\n\t\t\tfor (size_t i = 0; i + 2 < notificationUrl.length; ++i) {\n\t\t\t\tif (notificationUrl[i] == ':' && notificationUrl[i+1] == '/' && notificationUrl[i+2] == '/') {\n\t\t\t\t\tpos = cast(ptrdiff_t)i;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (pos >= 0) schemePos = cast(size_t)pos;\n\t\t}\n\n\t\tstring hostAndAfter;\n\t\tif (schemePos < notificationUrl.length) {\n\t\t\thostAndAfter = notificationUrl[(schemePos + 3) .. notificationUrl.length];\n\t\t} else {\n\t\t\thostAndAfter = notificationUrl;\n\t\t}\n\n\t\tsize_t slash = hostAndAfter.length;\n\t\tforeach (i; 0 .. hostAndAfter.length) {\n\t\t\tif (hostAndAfter[i] == '/') { slash = i; break; }\n\t\t}\n\n\t\tstring host = (slash < hostAndAfter.length) ? hostAndAfter[0 .. slash] : hostAndAfter;\n\t\tstring query = \"\";\n\t\tif (slash < hostAndAfter.length) {\n\t\t\tauto rest = hostAndAfter[slash .. hostAndAfter.length];\n\t\t\tsize_t qpos = rest.length;\n\t\t\tforeach (i; 0 .. rest.length) { if (rest[i] == '?') { qpos = i; break; } }\n\t\t\tif (qpos < rest.length) query = rest[(qpos + 1) .. rest.length];\n\t\t}\n\n\t\tstring outUrl = \"wss://\" ~ host ~ \"/socket.io/?EIO=4&transport=websocket\";\n\t\tif (query.length > 0) outUrl ~= \"&\" ~ query;\n\n\t\tlogSocketIOOutput(\"toSocketIoWsUrl output: \" ~ outUrl);\n\t\treturn outUrl;\n\t}\n\n\t// Wait for Socket.IO to open\n\tstatic bool awaitEngineOpen(curlWebsockets.CurlWebSocket ws, OneDriveSocketIo self) {\n\t\tSysTime deadline = Clock.currTime(UTC()) + dur!\"seconds\"(10);\n\t\tfor (;;) {\n\t\t\tif (Clock.currTime(UTC()) >= deadline) return false;\n\n\t\t\tauto msg = ws.recvText();\n\t\t\tif (msg.length == 0) {\n\t\t\t\tThread.sleep(dur!\"msecs\"(25));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (msg.length > 0 && msg[0] == '0') {\n\t\t\t\treturn parseEngineOpenFromPacket(msg, self);\n\t\t\t}\n\t\t\tif (msg.length > 1 && msg[0] == '4' && msg[1] == '0') {\n\t\t\t\tself.namespaceOpened = true;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tlogSocketIOOutput(\"Pre-open RX: \" ~ msg);\n\t\t}\n\t}\n\n\t// Parse Socket.IO response\n\tstatic bool parseEngineOpenFromPacket(string packet, OneDriveSocketIo self) {\n\t\t// packet = \"0{...json...}\"\n\t\tif (packet.length < 2) return false;\n\t\tauto jsonPart = packet[1 .. packet.length];\n\n\t\tJSONValue j;\n\t\tauto err = collectException(j = parseJSON(jsonPart));\n\t\tif (err !is null) {\n\t\t\tlogSocketIOOutput(\"Failed to parse Socket.IO open JSON\");\n\t\t\treturn false;\n\t\t}\n\n\t\tif (j.type == JSONType.object) {\n\t\t\t// sid\n\t\t\tif (\"sid\" in j.object) {\n\t\t\t\tauto vsid = j[\"sid\"];\n\t\t\t\tif (vsid.type == JSONType.string) {\n\t\t\t\t\tself.engineSid = vsid.str;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// pingInterval\n\t\t\tif (\"pingInterval\" in j.object) {\n\t\t\t\tauto v = j[\"pingInterval\"];\n\t\t\t\tif (v.type == JSONType.integer) {\n\t\t\t\t\tself.pingIntervalMs = v.integer;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// pingTimeout\n\t\t\tif (\"pingTimeout\" in j.object) {\n\t\t\t\tauto v2 = j[\"pingTimeout\"];\n\t\t\t\tif (v2.type == JSONType.integer) {\n\t\t\t\t\tself.pingTimeoutMs = v2.integer;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Log that we have opened a connection and have a valid SID\n\t\tlogSocketIOOutput(\"Engine open; sid=\" ~ self.engineSid ~ \" pingInterval=\" ~ self.pingIntervalMs.to!string ~ \"ms\" ~ \" pingTimeout=\"  ~ self.pingTimeoutMs.to!string  ~ \"ms\");\n\t\treturn true;\n\t}\n\n\t// Handle Socket.IO Events\n\tstatic void handleSocketIoEvent(string msg, OneDriveSocketIo self) {\n\t\t// Accept both: 42[...]\n\t\t// and:         42/<namespace>,[...]\n\t\tsize_t i = 2;\n\t\tstring ns = \"/\";\n\n\t\t// Optional namespace: 42/notifications,[...]\n\t\tif (i < msg.length && msg[i] == '/') {\n\t\t\tsize_t j = i + 1;\n\t\t\twhile (j < msg.length && msg[j] != ',') j++;\n\t\t\tif (j >= msg.length) {\n\t\t\t\tlogSocketIOOutput(\"42 frame (malformed namespace): \" ~ msg);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tns = msg[(i + 1) .. j];\n\t\t\ti = j + 1; // payload starts after comma\n\t\t}\n\n\t\tif (i >= msg.length || msg[i] != '[') {\n\t\t\tlogSocketIOOutput(\"42 frame (unexpected payload start): ns='/\" ~ ns ~ \"' raw=\" ~ msg);\n\t\t\treturn;\n\t\t}\n\n\t\tJSONValue arr;\n\t\tauto ex = collectException(arr = parseJSON(msg[i .. $]));\n\t\tif (ex !is null || arr.type != JSONType.array || arr.array.length == 0) {\n\t\t\tlogSocketIOOutput(\"42 frame (unparsed): ns='/\" ~ ns ~ \"' raw=\" ~ msg);\n\t\t\treturn;\n\t\t}\n\n\t\tauto evNameVal = arr.array[0];\n\t\tif (evNameVal.type != JSONType.string) {\n\t\t\tlogSocketIOOutput(\"42 frame (no string event name): ns='/\" ~ ns ~ \"' raw=\" ~ msg);\n\t\t\treturn;\n\t\t}\n\t\tstring evName = evNameVal.str;\n\n\t\t// 2nd element may be a JSON string containing the real JSON\n\t\tstring dataText = \"null\";\n\t\tif (arr.array.length > 1) {\n\t\t\tauto d = arr.array[1];\n\t\t\tif (d.type == JSONType.string) {\n\t\t\t\tJSONValue inner;\n\t\t\t\tauto ex2 = collectException(inner = parseJSON(d.str));\n\t\t\t\tif (ex2 is null) {\n\t\t\t\t\tdataText = inner.toString(); // normalized JSON\n\t\t\t\t} else {\n\t\t\t\t\tdataText = d.str;           // raw string if not JSON\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdataText = d.toString();\n\t\t\t}\n\t\t}\n\n\t\tif (evName == \"notification\") {\n\t\t\tlogSocketIOOutput(\"Notification Event (ns='/\" ~ ns ~ \"') -> \" ~ dataText);\n\t\t\t// Signal main() monitor loop exactly like webhook does\n\t\t\tcollectException(send(self.parentTid, cast(ulong)1));\n\t\t} else {\n\t\t\t// Visibility in case the service uses other event names\n\t\t\tlogSocketIOOutput(\"Event '\" ~ evName ~ \"' (ns='/\" ~ ns ~ \"') -> \" ~ dataText);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/sqlite.d",
    "content": "// What is this module called?\nmodule sqlite;\n\n// What does this module require to function?\nimport std.stdio;\nimport etc.c.sqlite3;\nimport std.string: fromStringz, toStringz;\nimport core.stdc.stdlib;\nimport std.conv;\nimport std.format;\nimport std.file;\n\n// What other modules that we have created do we need to import?\nimport log;\nimport util;\n\nextern (C) immutable(char)* sqlite3_errstr(int); // missing from the std library\n\n// Callback function to check if table exists\nextern (C) int tableExistsCallback(void* data, int argc, char** argv, char** colNames) {\n    // Set `tableExists` to 1 if at least one row is returned\n    int* tableExists = cast(int*) data;\n    *tableExists = 1;\n    return 0; // Continue processing\n}\n\nstatic this() {\n\tif (sqlite3_libversion_number() < 3006019) {\n\t\tthrow new SqliteException(-1, \"SQLite 3.6.19 or newer is required\");\n\t}\n}\n\nprivate string ifromStringz(const(char)* cstr) {\n\treturn fromStringz(cstr).idup;\n}\n\nclass SqliteException: Exception {\n\tint errorCode; // Add an errorCode member to store the SQLite error code\n\t@safe pure nothrow this(int errorCode, string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {\n        super(msg, file, line, next);\n\t\tthis.errorCode = errorCode; // Set the errorCode\n    }\n\n    @safe pure nothrow this(int errorCode, string msg, Throwable next, string file = __FILE__, size_t line = __LINE__) {\n        super(msg, file, line, next);\n\t\tthis.errorCode = errorCode; // Set the errorCode\n    }\n}\n\nstruct Database {\n\tprivate sqlite3* pDb;\n\n\tthis(const(char)[] filename) {\n\t\topen(filename);\n\t}\n\n\t~this() {\n\t\tclose();\n\t}\n\n\tint db_checkpoint() {\n\t\treturn sqlite3_wal_checkpoint(pDb, null);\n\t}\n\n\t// Dump open statements\n\tvoid dump_open_statements() {\n\t\tif (debugLogging) {addLogEntry(\"Dumping open SQL statements:\", [\"debug\"]);}\n\t\tauto p = sqlite3_next_stmt(pDb, null);\n\t\twhile (p != null) {\n\t\t\tif (debugLogging) {addLogEntry(\" Still Open: \" ~ to!string(ifromStringz(sqlite3_sql(p))), [\"debug\"]);}\n\t\t\tp = sqlite3_next_stmt(pDb, p);\n\t\t}\n\t}\n\t\n\t// Close open statements\n\tvoid close_open_statements() {\n\t\tif (debugLogging) {addLogEntry(\"Closing open SQL statements:\", [\"debug\"]);}\n\t\tauto p = sqlite3_next_stmt(pDb, null);\n\t\twhile (p != null) {\n\t\t\t// The sqlite3_finalize() function is called to delete a prepared statement\n\t\t\tsqlite3_finalize(p);\n\t\t\taddLogEntry(\" Finalised:  \" ~ to!string(ifromStringz(sqlite3_sql(p))));\n\t\t\tp = sqlite3_next_stmt(pDb, p);\n\t\t}\n\t}\n\t\n\t// Count open statements\n\tint count_open_statements() {\n\t\tif (debugLogging) {addLogEntry(\"Counting open SQL statements\", [\"debug\"]);}\n\t\tint openStatementCount = 0;\n\t\tauto p = sqlite3_next_stmt(pDb, null);\n\t\twhile (p != null) {\n\t\t\topenStatementCount++;\n\t\t\tp = sqlite3_next_stmt(pDb, p);\n\t\t}\n\t\treturn openStatementCount;\n\t}\n\t\n\t// Check DB Status\n\tvoid checkStatus() {\n        int rc = sqlite3_errcode(pDb);\n        if (rc != SQLITE_OK) {\n            throw new SqliteException(rc, getErrorMessage());\n        }\n    }\n\t\n\t// Open the database file\n\tvoid open(const(char)[] filename) {\n\t\t// https://www.sqlite.org/c3ref/open.html\n\t\t// Safest multithreaded way to open the database\n\t\t\n\t\t// Does the file we need to open actually exist?\n\t\tif (exists(filename)) {\n\t\t\tif (debugLogging) {addLogEntry(\"Database file EXISTS on disk\", [\"debug\"]);}\n\t\t} else {\n\t\t\tif (debugLogging) {addLogEntry(\"Database file DOES NOT EXIST on disk\", [\"debug\"]);}\n\t\t}\n\t\t\n\t\tint rc = sqlite3_open_v2(\n\t\t\ttoStringz(filename), /* Database filename (UTF-8) */\n\t\t\t&pDb,                /* OUT: SQLite db handle */\n\t\t\tSQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,  /* Flags */\n\t\t\tnull                 /* Optional: Name of the VFS module to use */\n\t\t);\n\t\t\n\t\tif (rc != SQLITE_OK) {\n\t\t\tstring errorMsg;\n\t\t\tif (rc == SQLITE_CANTOPEN) {\n\t\t\t\t// Database cannot be opened\n\t\t\t\terrorMsg = \"The database cannot be opened. Please check the permissions of \" ~ to!string(filename);\n\t\t\t} else {\n\t\t\t\t// Some other error\n\t\t\t\terrorMsg = \"A database access error occurred: \" ~ getErrorMessage();\n\t\t\t}\n\t\t\t\n\t\t\t// Log why we could not open the database file\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(errorMsg);\n\t\t\taddLogEntry();\n\t\t\t\n\t\t\tclose();\n\t\t\tthrow new SqliteException(rc, getErrorMessage());\n\t\t}\n\t\t\n\t\t// Opened database file OK\n\t\t// Flag to always use extended result codes for errors\n\t\tsqlite3_extended_result_codes(pDb, 1);\n\t}\n\n\tvoid exec(const(char)[] sql) {\n\t\t// https://www.sqlite.org/c3ref/exec.html\n\t\tif (pDb !is null) {\n\t\t\tint rc = sqlite3_exec(pDb, toStringz(sql), null, null, null);\n\t\t\tif (rc != SQLITE_OK) {\n\t\t\t\t// Get error message and print it, then exit\n\t\t\t\tstring errorMessage = getErrorMessage();\n\t\t\t\tclose();\n\t\t\t\t// Throw sqlite error\n\t\t\t\tthrow new SqliteException(rc, errorMessage);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Check if the table exists before dropping it\n\tvoid dropTableIfExists(const(char)[] tableName) {\n\t\tstring checkTableQuery = \"SELECT name FROM sqlite_master WHERE type='table' AND name='\" ~ to!string(tableName) ~ \"';\";\n\t\tint tableExists = 0;\n\t\t\n\t\t// Execute query with callback to check if table exists\n\t\tint rc = sqlite3_exec(pDb, toStringz(checkTableQuery), &tableExistsCallback, &tableExists, null);\n\n\t\t// Only proceed if the query executed successfully\n\t\tif (rc == SQLITE_OK) {\n\t\t\t// If the table exists, drop it\n\t\t\tif (tableExists == 1) {\n\t\t\t\texec(\"DROP TABLE \" ~ tableName);\n\t\t\t} else {\n\t\t\t\t// Optionally log that the table does not exist\n\t\t\t\taddLogEntry(format(\"WARNING: Table '%s' does not exist, skipping table drop.\", to!string(tableName)));\n\t\t\t}\n\t\t} else {\n\t\t\t// Log or handle the error if `sqlite3_exec` fails\n\t\t\taddLogEntry(format(\"ERROR: Failed to execute table existence check for '%s'.\", to!string(tableName)));\n\t\t}\n\t}\n\t\n\t// Get DB Version\n\tint getVersion() {\n\t\tint userVersion;\n\t\textern (C) int callback(void* user_version, int count, char** column_text, char** column_name) {\n\t\t\timport core.stdc.stdlib: atoi;\n\t\t\t*(cast(int*) user_version) = atoi(*column_text);\n\t\t\treturn 0;\n\t\t}\n\t\tint rc = sqlite3_exec(pDb, \"PRAGMA user_version\", &callback, &userVersion, null);\n\t\tif (rc != SQLITE_OK) {\n\t\t\tthrow new SqliteException(rc, getErrorMessage());\n\t\t}\n\t\treturn userVersion;\n\t}\n\t\n\t// Get the threadsafe value\n\tint getThreadsafeValue() {\n\t\treturn sqlite3_threadsafe();\n\t}\n\n\t// Get sqlite error message\n\tstring getErrorMessage() {\n\t\treturn ifromStringz(sqlite3_errmsg(pDb));\n\t}\n\t\n\tvoid setVersion(int userVersion) {\n\t\texec(\"PRAGMA user_version=\" ~ to!string(userVersion));\n\t}\n\n\tStatement prepare(const(char)[] zSql) {\n\t\tStatement s;\n\t\t// https://www.sqlite.org/c3ref/prepare.html\n\t\tif (pDb !is null) {\n\t\t\tint rc = sqlite3_prepare_v2(pDb, zSql.ptr, cast(int) zSql.length, &s.pStmt, null);\n\t\t\tif (rc != SQLITE_OK) {\n\t\t\t\tthrow new SqliteException(rc, getErrorMessage());\n\t\t\t}\n\t\t}\n\t\treturn s;\n\t}\n\n\tvoid close() {\n\t\t// https://www.sqlite.org/c3ref/close.html\n\t\tif (pDb !is null) {\n\t\t\tsqlite3_close_v2(pDb);\n\t\t\tpDb = null;\n\t\t}\n\t}\n}\n\nstruct Statement {\n\tstruct Result {\n\t\tprivate sqlite3_stmt* pStmt;\n\t\tprivate const(char)[][] row;\n\n\t\tprivate this(sqlite3_stmt* pStmt) {\n\t\t\tthis.pStmt = pStmt;\n\t\t\tstep(); // initialize the range\n\t\t}\n\n\t\t@property bool empty() {\n\t\t\treturn row.length == 0;\n\t\t}\n\n\t\t@property auto front() {\n\t\t\treturn row;\n\t\t}\n\n\t\talias step popFront;\n\n\t\tvoid step() {\n\t\t\t// https://www.sqlite.org/c3ref/step.html\n\t\t\tint rc = sqlite3_step(pStmt);\n\t\t\tif (rc == SQLITE_BUSY) {\n\t\t\t\t// Database is locked by another onedrive process\n\t\t\t\taddLogEntry(\"The database is currently locked by another process - cannot sync\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (rc == SQLITE_DONE) {\n\t\t\t\trow.length = 0;\n\t\t\t} else if (rc == SQLITE_ROW) {\n\t\t\t\t// https://www.sqlite.org/c3ref/data_count.html\n\t\t\t\tint count = 0;\n\t\t\t\tcount = sqlite3_data_count(pStmt);\n\t\t\t\trow = new const(char)[][count];\n\t\t\t\tforeach (size_t i, ref column; row) {\n\t\t\t\t\t// https://www.sqlite.org/c3ref/column_blob.html\n\t\t\t\t\tcolumn = fromStringz(sqlite3_column_text(pStmt, to!int(i)));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tstring errorMessage = getErrorMessage();\n\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\tthrow new SqliteException(rc, errorMessage);\n\t\t\t}\n\t\t}\n\t\t\n\t\tstring getErrorMessage() {\n\t\t\treturn ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt)));\n\t\t}\t\n\t}\n\n\tprivate sqlite3_stmt* pStmt;\n\n\t~this() {\n\t\t// Finalise any prepared statement\n\t\tfinalise();\n\t}\n\t\n\t// https://www.sqlite.org/c3ref/finalize.html\n\tvoid finalise() {\n\t\tif (pStmt !is null) {\n\t\t\t// The sqlite3_finalize() function is called to delete a prepared statement\n\t\t\tsqlite3_finalize(pStmt);\n\t\t\tpStmt = null;\n\t\t}\n\t}\n\n\tvoid bind(int index, const(char)[] value) {\n\t\treset();\n\t\t// https://www.sqlite.org/c3ref/bind_blob.html\n\t\tint rc = sqlite3_bind_text(pStmt, index, value.ptr, cast(int) value.length, SQLITE_STATIC);\n\t\tif (rc != SQLITE_OK) {\n\t\t\tthrow new SqliteException(rc, getErrorMessage());\n\t\t}\n\t}\n\n\tResult exec() {\n\t\treset();\n\t\treturn Result(pStmt);\n\t}\n\t\n\tprivate void reset() {\n\t\t// https://www.sqlite.org/c3ref/reset.html\n\t\tint rc = sqlite3_reset(pStmt);\n\t\tif (rc != SQLITE_OK) {\n\t\t\tthrow new SqliteException(rc, getErrorMessage());\n\t\t}\n\t}\n\t\n\tstring getErrorMessage() {\n\t\treturn ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt)));\n\t}\n}"
  },
  {
    "path": "src/sync.d",
    "content": "// What is this module called?\nmodule syncEngine;\n\n// What does this module require to function?\nimport core.memory;\nimport core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;\nimport core.thread;\nimport core.time;\nimport std.algorithm;\nimport std.array;\nimport std.concurrency;\nimport std.container.rbtree;\nimport std.conv;\nimport std.datetime;\nimport std.encoding;\nimport std.exception;\nimport std.file;\nimport std.json;\nimport std.parallelism;\nimport std.path;\nimport std.range;\nimport std.regex;\nimport std.stdio;\nimport std.string;\nimport std.uni;\nimport std.uri;\nimport std.utf;\nimport std.math;\n\nimport std.typecons;\n\n// What other modules that we have created do we need to import?\nimport config;\nimport log;\nimport util;\nimport onedrive;\nimport itemdb;\nimport clientSideFiltering;\nimport xattr;\n\nclass JsonResponseException: Exception {\n\t@safe pure this(string inputMessage) {\n\t\tstring msg = format(inputMessage);\n\t\tsuper(msg);\n\t}\n}\n\nclass PosixException: Exception {\n\t@safe pure this(string localTargetName, string remoteTargetName) {\n\t\tstring msg = format(\"POSIX 'case-insensitive match' between '%s' (local) and '%s' (online) which violates the Microsoft OneDrive API namespace convention\", localTargetName, remoteTargetName);\n\t\tsuper(msg);\n\t}\n}\n\nclass AccountDetailsException: Exception {\n\t@safe pure this() {\n\t\tstring msg = format(\"Unable to query OneDrive API to obtain required account details\");\n\t\tsuper(msg);\n\t}\n}\n\nclass SyncException: Exception {\n    @nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__) {\n        super(msg, file, line);\n    }\n}\n\nstruct DriveDetailsCache {\n\t// - driveId is the drive for the operations were items need to be stored\n\t// - 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\n\t// - quotaAvailable is a long value that stores the value of what the current free space is available online\n\tstring driveId;\n\tbool quotaRestricted;\n\tbool quotaAvailable;\n\tlong quotaRemaining;\n}\n\nstruct DeltaLinkDetails {\n\tstring driveId;\n\tstring itemId;\n\tstring latestDeltaLink;\n}\n\nstruct DatabaseItemsToDeleteOnline {\n\tItem dbItem;\n\tstring localFilePath;\n}\n\nclass SyncEngine {\n\t// Class Variables\n\tApplicationConfig appConfig;\n\tItemDatabase itemDB;\n\tClientSideFiltering selectiveSync;\n\t\n\t// Array of directory databaseItem.id to skip while applying the changes.\n\t// 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\n\tRedBlackTree!string skippedItems = redBlackTree!string();\n\t\n\t// Array consisting of 'item.driveId', 'item.id' and 'item.parentId' values to delete after all the online changes have been downloaded\n\tstring[3][] idsToDelete;\n\t// Array of JSON items which are files or directories that are not 'root', skipped or to be deleted, that need to be processed\n\tJSONValue[] jsonItemsToProcess;\n\t// Array of JSON items which are files that are not 'root', skipped or to be deleted, that need to be downloaded\n\tJSONValue[] fileJSONItemsToDownload;\n\t// Array of paths that failed to download\n\tstring[] fileDownloadFailures;\n\t// Associative array mapping of all OneDrive driveId's that have been seen, mapped with DriveDetailsCache data for reference\n\tDriveDetailsCache[string] onlineDriveDetails;\n\t// List of items we fake created when using --dry-run\n\tstring[2][] idsFaked;\n\t// List of paths we fake deleted when using --dry-run\n\tstring[] pathFakeDeletedArray;\n\t// Array of database Parent Item ID, Item ID & Local Path where the content has changed and needs to be uploaded\n\tstring[3][] databaseItemsWhereContentHasChanged;\n\t// Array of local file paths that need to be uploaded as new items to OneDrive\n\tstring[] newLocalFilesToUploadToOneDrive;\n\t// Array of local file paths that failed to be uploaded to OneDrive\n\tstring[] fileUploadFailures;\n\t// List of path names changed online, but not changed locally when using --dry-run\n\tstring[] pathsRenamed;\n\t// List of path names retained when using --download-only --cleanup-local-files + using a 'sync_list'\n\tstring[] pathsRetained;\n\t// List of paths that were a POSIX case-insensitive match, thus could not be created online\n\tstring[] posixViolationPaths;\n\t// List of local paths, that, when using the OneDrive Business Shared Folders feature, then disabling it, folder still exists locally and online\n\t// This list of local paths need to be skipped\n\tstring[] businessSharedFoldersOnlineToSkip;\n\t// List of interrupted uploads session files that need to be resumed\n\tstring[] interruptedUploadsSessionFiles;\n\t// List of interrupted downloads that need to be resumed\n\tstring[] interruptedDownloadFiles;\n\t// List of validated interrupted uploads session JSON items to resume\n\tJSONValue[] jsonItemsToResumeUpload;\n\t// List of validated interrupted download JSON items to resume\n\tJSONValue[] jsonItemsToResumeDownload;\n\t// This list of local paths that need to be created online\n\tstring[] pathsToCreateOnline;\n\t// Array of items from the database that have been deleted locally, that needs to be deleted online\n\tDatabaseItemsToDeleteOnline[] databaseItemsToDeleteOnline;\n\t// Array of parentId's that have been skipped via 'sync_list'\n\tstring[] syncListSkippedParentIds;\n\t// Array of Microsoft OneNote Notebook Package ID's\n\tstring[] onenotePackageIdentifiers;\n\t\t\n\t// Flag that there were upload or download failures listed\n\tbool syncFailures = false;\n\t// Is sync_list configured\n\tbool syncListConfigured = false;\n\t// Was --dry-run used?\n\tbool dryRun = false;\n\t// Was --upload-only used?\n\tbool uploadOnly = false;\n\t// Was --remove-source-files used?\n\t// Flag to set whether the local file should be deleted once it is successfully uploaded to OneDrive\n\tbool localDeleteAfterUpload = false;\n\t\n\t// Do we configure to disable the download validation routine due to --disable-download-validation\n\t// We will always validate our downloads\n\t// However, when downloading files from SharePoint, the OneDrive API will not advise the correct file size \n\t// which means that the application thinks the file download has failed as the size is different / hash is different\n\t// See: https://github.com/abraunegg/onedrive/discussions/1667\n    bool disableDownloadValidation = false;\n\t\n\t// Do we configure to disable the upload validation routine due to --disable-upload-validation\n\t// We will always validate our uploads\n\t// However, when uploading a file that can contain metadata SharePoint will associate some \n\t// metadata from the library the file is uploaded to directly in the file which breaks this validation. \n\t// See: https://github.com/abraunegg/onedrive/issues/205\n\t// See: https://github.com/OneDrive/onedrive-api-docs/issues/935\n\tbool disableUploadValidation = false;\n\t\n\t// Do we perform a local cleanup of files that are 'extra' on the local file system, when using --download-only\n\tbool cleanupLocalFiles = false;\n\t// Are we performing a --single-directory sync ?\n\tbool singleDirectoryScope = false;\n\tstring singleDirectoryScopeDriveId;\n\tstring singleDirectoryScopeItemId;\n\t// Is National Cloud Deployments configured ?\n\tbool nationalCloudDeployment = false;\n\t// Do we configure not to perform a remote file delete if --upload-only & --no-remote-delete configured\n\tbool noRemoteDelete = false;\n\t// Is bypass_data_preservation set via config file\n\t// Local data loss MAY occur in this scenario\n\tbool bypassDataPreservation = false;\n\t// Has the user configured to permanently delete files online rather than send to online recycle bin\n\tbool permanentDelete = false;\n\t// Maximum file size upload\n\t//  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\n\t//\tJuly 2020, maximum file size for all accounts is 100GB\n\t//  January 2021, maximum file size for all accounts is 250GB\n\tlong maxUploadFileSize = 268435456000; // 250GB\n\t// Threshold after which files will be uploaded using an upload session\n\tlong sessionThresholdFileSize = 4 * 2^^20; // 4 MiB\n\t// File size limit for file operations that the user has configured\n\tlong fileSizeLimit;\n\t// Total data to upload\n\tlong totalDataToUpload;\n\t// How many items have been processed for the active operation\n\tlong processedCount;\n\t// Are we creating a simulated /delta response? This is critically important in terms of how we 'update' the database\n\tbool generateSimulatedDeltaResponse = false;\n\t// Store the latest DeltaLink\n\tstring latestDeltaLink;\n\t// Struct of containing the deltaLink details\n\tDeltaLinkDetails deltaLinkCache;\n\t// Array of driveId and deltaLink for use when performing the last examination of the most recent online data\n\talias DeltaLinkInfo = string[string];\n\tDeltaLinkInfo deltaLinkInfo;\n\t// Flag to denote data cleanup pass when using --download-only --cleanup-local-files\n\tbool cleanupDataPass = false;\n\t// Create the specific task pool to process items in parallel\n\tTaskPool processPool;\n\t\n\t// Shared Folder Flags for 'sync_list' processing\n\tbool sharedFolderDeltaGeneration = false;\n\tstring currentSharedFolderName = \"\";\n\t\n\t// Directory excluded by 'sync_list flag so that when scanning that directory, if it is excluded, \n\t// can be scanned for new data which may be included by other include rule, but parent is excluded\n\tbool syncListDirExcluded = false;\n\t\n\t// Debug Logging Break Lines\n\tstring debugLogBreakType1 = \"-----------------------------------------------------------------------------------------------------------\";\n\tstring debugLogBreakType2 = \"===========================================================================================================\";\n\t\n\t// Configure this class instance\n\tthis(ApplicationConfig appConfig, ItemDatabase itemDB, ClientSideFiltering selectiveSync) {\n\t\n\t\t// Create the specific task pool to process items in parallel\n\t\tprocessPool = new TaskPool(to!int(appConfig.getValueLong(\"threads\")));\n\t\tif (debugLogging) {addLogEntry(\"Initialised TaskPool worker with threads: \" ~ to!string(processPool.size), [\"debug\"]);}\n\t\t\n\t\t// Configure the class variable to consume the application configuration\n\t\tthis.appConfig = appConfig;\n\t\t// Configure the class variable to consume the database configuration\n\t\tthis.itemDB = itemDB;\n\t\t// Configure the class variable to consume the selective sync (skip_dir, skip_file and sync_list) configuration\n\t\tthis.selectiveSync = selectiveSync;\n\t\t\n\t\t// Configure the dryRun flag to capture if --dry-run was used\n\t\t// Application startup already flagged we are also in a --dry-run state, so no need to output anything else here\n\t\tthis.dryRun = appConfig.getValueBool(\"dry_run\");\n\t\t\n\t\t// Configure file size limit\n\t\tif (appConfig.getValueLong(\"skip_size\") != 0) {\n\t\t\tfileSizeLimit = appConfig.getValueLong(\"skip_size\") * 2^^20;\n\t\t\tfileSizeLimit = (fileSizeLimit == 0) ? long.max : fileSizeLimit;\n\t\t}\n\t\t\n\t\t// Is there a sync_list file present?\n\t\tif (exists(appConfig.syncListFilePath)) {\n\t\t\t// yes there is a file present, but did we load any entries?\n\t\t\tif (!selectiveSync.validSyncListRules) {\n\t\t\t\t// function returned 'false' (array contains valid entries)\n\t\t\t\t// flag there are rules to process when we are performing Client Side Filtering\n\t\t\t\tif (debugLogging) {addLogEntry(\"Configuring syncListConfigured flag to TRUE as valid entries were loaded from 'sync_list' file\", [\"debug\"]);}\n\t\t\t\tthis.syncListConfigured = true;\n\t\t\t} else {\n\t\t\t\t// function returned 'true' meaning there are are zero sync_list rules loaded despite the 'sync_list' file being present\n\t\t\t\t// ensure this flag is false so we do not do any extra processing\n\t\t\t\tif (debugLogging) {addLogEntry(\"Configuring syncListConfigured flag to FALSE as no valid entries were loaded from 'sync_list' file\", [\"debug\"]);}\n\t\t\t\tthis.syncListConfigured = false;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Configure the uploadOnly flag to capture if --upload-only was used\n\t\tif (appConfig.getValueBool(\"upload_only\")) {\n\t\t\tif (debugLogging) {addLogEntry(\"Configuring uploadOnly flag to TRUE as --upload-only passed in or configured\", [\"debug\"]);}\n\t\t\tthis.uploadOnly = true;\n\t\t}\n\t\t\n\t\t// Configure the localDeleteAfterUpload flag\n\t\tif (appConfig.getValueBool(\"remove_source_files\")) {\n\t\t\tif (debugLogging) {addLogEntry(\"Configuring localDeleteAfterUpload flag to TRUE as --remove-source-files passed in or configured\", [\"debug\"]);}\n\t\t\tthis.localDeleteAfterUpload = true;\n\t\t}\n\t\t\n\t\t// Configure the disableDownloadValidation flag\n\t\tif (appConfig.getValueBool(\"disable_download_validation\")) {\n\t\t\tif (debugLogging) {addLogEntry(\"Configuring disableDownloadValidation flag to TRUE as --disable-download-validation passed in or configured\", [\"debug\"]);}\n\t\t\tthis.disableDownloadValidation = true;\n\t\t}\n\t\t\n\t\t// Configure the disableUploadValidation flag\n\t\tif (appConfig.getValueBool(\"disable_upload_validation\")) {\n\t\t\tif (debugLogging) {addLogEntry(\"Configuring disableUploadValidation flag to TRUE as --disable-upload-validation passed in or configured\", [\"debug\"]);}\n\t\t\tthis.disableUploadValidation = true;\n\t\t}\n\t\t\n\t\t// Do we configure to clean up local files if using --download-only ?\n\t\tif ((appConfig.getValueBool(\"download_only\")) && (appConfig.getValueBool(\"cleanup_local_files\"))) {\n\t\t\t// --download-only and --cleanup-local-files were passed in\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"WARNING: Application has been configured to cleanup local files that are not present online.\");\n\t\t\taddLogEntry(\"WARNING: Local data loss MAY occur in this scenario if you are expecting data to remain archived locally.\");\n\t\t\taddLogEntry();\n\t\t\t// Set the flag\n\t\t\tthis.cleanupLocalFiles = true;\n\t\t}\n\t\t\n\t\t// Do we configure to NOT perform a remote delete if --upload-only & --no-remote-delete configured ?\n\t\tif ((appConfig.getValueBool(\"upload_only\")) && (appConfig.getValueBool(\"no_remote_delete\"))) {\n\t\t\t// --upload-only and --no-remote-delete were passed in\n\t\t\taddLogEntry(\"WARNING: Application has been configured NOT to cleanup remote files that are deleted locally.\");\n\t\t\t// Set the flag\n\t\t\tthis.noRemoteDelete = true;\n\t\t}\n\t\t\n\t\t// Are we configured to use a National Cloud Deployment?\n\t\tif (appConfig.getValueString(\"azure_ad_endpoint\") != \"\") {\n\t\t\t// value is configured, is it a valid value?\n\t\t\tif ((appConfig.getValueString(\"azure_ad_endpoint\") == \"USL4\") || (appConfig.getValueString(\"azure_ad_endpoint\") == \"USL5\") || (appConfig.getValueString(\"azure_ad_endpoint\") == \"DE\") || (appConfig.getValueString(\"azure_ad_endpoint\") == \"CN\")) {\n\t\t\t\t// valid entries to flag we are using a National Cloud Deployment\n\t\t\t\t// National Cloud Deployments do not support /delta as a query\n\t\t\t\t// https://docs.microsoft.com/en-us/graph/deployments#supported-features\n\t\t\t\t// Flag that we have a valid National Cloud Deployment that cannot use /delta queries\n\t\t\t\tthis.nationalCloudDeployment = true;\n\t\t\t\t// Reverse set 'force_children_scan' for completeness\n\t\t\t\tappConfig.setValueBool(\"force_children_scan\", true);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Are we forcing to use /children scan instead of /delta to simulate National Cloud Deployment use of /children?\n\t\tif (appConfig.getValueBool(\"force_children_scan\")) {\n\t\t\taddLogEntry(\"Forcing client to use /children API call rather than /delta API to retrieve objects from the OneDrive API\");\n\t\t\tthis.nationalCloudDeployment = true;\n\t\t}\n\t\t\n\t\t// Are we forcing the client to bypass any data preservation techniques to NOT rename any local files if there is a conflict?\n\t\t// The enabling of this function could lead to data loss\n\t\tif (appConfig.getValueBool(\"bypass_data_preservation\")) {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"WARNING: Application has been configured to bypass local data preservation in the event of file conflict.\");\n\t\t\taddLogEntry(\"WARNING: Local data loss MAY occur in this scenario.\");\n\t\t\taddLogEntry();\n\t\t\tthis.bypassDataPreservation = true;\n\t\t}\n\t\t\n\t\t// Did the user configure a specific rate limit for the application?\n\t\tif (appConfig.getValueLong(\"rate_limit\") > 0) {\n\t\t\t// User configured rate limit\n\t\t\taddLogEntry(\"User Configured Rate Limit: \" ~ to!string(appConfig.getValueLong(\"rate_limit\")));\n\t\t\t\n\t\t\t// If user provided rate limit is < 131072, flag that this is too low, setting to the recommended minimum of 131072\n\t\t\tif (appConfig.getValueLong(\"rate_limit\") < 131072) {\n\t\t\t\t// user provided limit too low\n\t\t\t\taddLogEntry(\"WARNING: User configured rate limit too low for normal application processing and preventing application timeouts. Overriding to recommended minimum of 131072 (128KB/s)\");\n\t\t\t\tappConfig.setValueLong(\"rate_limit\", 131072);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Did the user downgrade all HTTP operations to force HTTP 1.1\n\t\tif (appConfig.getValueBool(\"force_http_11\")) {\n\t\t\t// User is forcing downgrade to curl to use HTTP 1.1 for all operations\n\t\t\tif (verboseLogging) {addLogEntry(\"Downgrading all HTTP operations to HTTP/1.1 due to user configuration\", [\"verbose\"]);}\n\t\t} else {\n\t\t\t// Use curl defaults\n\t\t\tif (debugLogging) {addLogEntry(\"Using Curl defaults for HTTP operational protocol version (potentially HTTP/2)\", [\"debug\"]);}\n\t\t}\n\t}\n\t\n\t// Initialise the Sync Engine class\n\tbool initialise() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Control whether the worker threads are daemon threads. A daemon thread is automatically terminated when all non-daemon threads have terminated.\n\t\tprocessPool.isDaemon(true); // daemon thread\n\t\t\n\t\t// Flag for 'no-sync' task\n\t\tbool noSyncTask = false;\n\t\t\n\t\t// Create a new instance of the OneDrive API\n\t\tOneDriveApi oneDriveApiInstance;\n\t\toneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t// Exit scope - release curl engine back to pool\n\t\tscope(exit) {\n\t\t\toneDriveApiInstance.releaseCurlEngine();\n\t\t\t// Free object and memory\n\t\t\toneDriveApiInstance = null;\n\t\t}\n\t\t\n\t\t// Issue #2941\n\t\t// If the account being used _only_ has access to specific resources, getDefaultDriveDetails() will generate problems and cause\n\t\t// the application to exit, which, is technically the right thing to do (no access to account details) ... but if:\n\t\t// - are we doing a no-sync task ?\n\t\t// - do we have the 'drive_id' via config file ?\n\t\t// Are we not doing a --sync or a --monitor operation? Both of these will be false if they are not set\n\t\tif ((!appConfig.getValueBool(\"synchronize\")) && (!appConfig.getValueBool(\"monitor\"))) {\n\t\t\t// set flag\n\t\t\tnoSyncTask = true;\n\t\t}\n\t\t\n\t\t// Can the API be initialised successfully?\n\t\tif (oneDriveApiInstance.initialise()) {\n\t\t\t// Get the relevant default drive details\n\t\t\ttry {\n\t\t\t\tgetDefaultDriveDetails();\n\t\t\t} catch (AccountDetailsException exception) {\n\t\t\t\t// was this a no-sync task?\n\t\t\t\tif (!noSyncTask) {\n\t\t\t\t\t// details could not be queried\n\t\t\t\t\taddLogEntry(exception.msg);\n\t\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\t\tforceExit();\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Get the relevant default root details\n\t\t\ttry {\n\t\t\t\tgetDefaultRootDetails();\n\t\t\t} catch (AccountDetailsException exception) {\n\t\t\t\t// details could not be queried\n\t\t\t\taddLogEntry(exception.msg);\n\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\tforceExit();\n\t\t\t}\n\t\t\t\n\t\t\t// Display relevant account details\n\t\t\ttry {\n\t\t\t\t// we only do this if we are doing --verbose logging\n\t\t\t\tif (verboseLogging) {\n\t\t\t\t\tdisplaySyncEngineDetails();\n\t\t\t\t}\n\t\t\t} catch (AccountDetailsException exception) {\n\t\t\t\t// details could not be queried\n\t\t\t\taddLogEntry(exception.msg);\n\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\tforceExit();\n\t\t\t}\n\t\t} else {\n\t\t\t// API could not be initialised\n\t\t\taddLogEntry(\"OneDrive API could not be initialised with previously used details\");\n\t\t\t// Must force exit here, allow logging to be done\n\t\t\tforceExit();\n\t\t}\n\t\t\n\t\t// Has the client been configured to permanently delete files online rather than send these to the online recycle bin?\n\t\tif (appConfig.getValueBool(\"permanent_delete\")) {\n\t\t\t// This can only be set if not using:\n\t\t\t// - US Government L4\n\t\t\t// - US Government L5 (DOD)\n\t\t\t// - Azure and Office365 operated by VNET in China\n\t\t\t// \n\t\t\t// Additionally, this is not supported by OneDrive Personal accounts:\n\t\t\t//\n\t\t\t//   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.\n\t\t\t//\n\t\t\t// Reference: https://learn.microsoft.com/en-us/answers/questions/1501170/onedrive-permanently-delete-a-file\n\t\t\tstring azureConfigValue = appConfig.getValueString(\"azure_ad_endpoint\");\n\t\t\t\n\t\t\t// Now that we know the 'accountType' we can configure this correctly\n\t\t\tif ((appConfig.accountType != \"personal\") && (azureConfigValue.empty || azureConfigValue == \"DE\")) {\n\t\t\t\t// Only supported for Global Service and DE based on https://learn.microsoft.com/en-us/graph/api/driveitem-permanentdelete?view=graph-rest-1.0\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"WARNING: Application has been configured to permanently remove files online rather than send to the recycle bin. Permanently deleted items can't be restored.\");\n\t\t\t\taddLogEntry(\"WARNING: Online data loss MAY occur in this scenario.\");\n\t\t\t\taddLogEntry();\n\t\t\t\tthis.permanentDelete = true;\n\t\t\t} else {\n\t\t\t\t// what error message do we present\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t// personal account type - API not supported\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\taddLogEntry(\"WARNING: The application is configured to permanently delete files online; however, this action is not supported by Microsoft OneDrive Personal Accounts.\");\n\t\t\t\t\taddLogEntry();\n\t\t\t\t} else {\n\t\t\t\t\t// Not a personal account\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\taddLogEntry(\"WARNING: The application is configured to permanently delete files online; however, this action is not supported by the National Cloud Deployment in use.\");\n\t\t\t\t\taddLogEntry();\n\t\t\t\t}\n\t\t\t\t// ensure this is false regardless\n\t\t\t\tthis.permanentDelete = false;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// API was initialised\n\t\tif (verboseLogging) {addLogEntry(\"Sync Engine Initialised with new Onedrive API instance\", [\"verbose\"]);}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return required value\n\t\treturn true;\n\t}\n\t\n\t// Shutdown the sync engine, wait for anything in processPool to complete\n\tvoid shutdown() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\n\t\tif (debugLogging) {addLogEntry(\"SyncEngine: Waiting for all internal threads to complete\", [\"debug\"]);}\n\t\tshutdownProcessPool();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\t\n\t}\n\t\n\t// Shut down all running tasks that are potentially running in parallel\n\tvoid shutdownProcessPool() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// TaskPool needs specific shutdown based on compiler version otherwise this causes a segfault\n\t\tif (processPool.size > 0) {\n\t\t\t// TaskPool is still configured for 'thread' size\n\t\t\t// Normal TaskPool shutdown process\n\t\t\tif (debugLogging) {addLogEntry(\"Shutting down processPool in a thread blocking manner\", [\"debug\"]);}\n\t\t\t// All worker threads are daemon threads which are automatically terminated when all non-daemon threads have terminated.\n\t\t\tprocessPool.finish(true); // If blocking argument is true, wait for all worker threads to terminate before returning.\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\t\n\t// Get Default Drive Details for this Account\n\tvoid getDefaultDriveDetails() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Function variables\n\t\tJSONValue defaultOneDriveDriveDetails;\n\t\tbool noSyncTask = false;\n\t\t\n\t\t// Create a new instance of the OneDrive API\n\t\tOneDriveApi getDefaultDriveApiInstance;\n\t\tgetDefaultDriveApiInstance = new OneDriveApi(appConfig);\n\t\tgetDefaultDriveApiInstance.initialise();\n\t\t\n\t\t// Are we not doing a --sync or a --monitor operation? Both of these will be false if they are not set\n\t\tif ((!appConfig.getValueBool(\"synchronize\")) && (!appConfig.getValueBool(\"monitor\"))) {\n\t\t\t// set flag\n\t\t\tnoSyncTask = true;\n\t\t}\n\t\t\n\t\t// Get Default Drive Details for this Account\n\t\ttry {\n\t\t\tif (debugLogging) {addLogEntry(\"Getting Account Default Drive Details\", [\"debug\"]);}\n\t\t\tdefaultOneDriveDriveDetails = getDefaultDriveApiInstance.getDefaultDriveDetails();\n\t\t} catch (OneDriveException exception) {\n\t\t\tif (debugLogging) {addLogEntry(\"defaultOneDriveDriveDetails = getDefaultDriveApiInstance.getDefaultDriveDetails() generated a OneDriveException\", [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\tif ((exception.httpStatusCode == 400) || (exception.httpStatusCode == 401)) {\n\t\t\t\t// Handle the 400 | 401 error\n\t\t\t\thandleClientUnauthorised(exception.httpStatusCode, exception.error);\n\t\t\t} else {\n\t\t\t\t// Default operation if not 400,401 errors\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within getDefaultDriveApiInstance\n\t\t\t\t// Display what the error is\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// If the JSON response is a correct JSON object, and has an 'id' we can set these details\n\t\tif ((defaultOneDriveDriveDetails.type() == JSONType.object) && (hasId(defaultOneDriveDriveDetails))) {\n\t\t\tif (debugLogging) {addLogEntry(\"OneDrive Account Default Drive Details:      \" ~ sanitiseJSONItem(defaultOneDriveDriveDetails), [\"debug\"]);}\n\t\t\tappConfig.accountType = defaultOneDriveDriveDetails[\"driveType\"].str;\n\t\t\t\n\t\t\t// Issue #3115 - Validate driveId length\n\t\t\t// What account type is this?\n\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t// Test driveId length and validation\n\t\t\t\t// Once checked and validated, we only need to check 'driveId' if it does not match exactly 'appConfig.defaultDriveId'\n\t\t\t\tappConfig.defaultDriveId = transformToLowerCase(testProvidedDriveIdForLengthIssue(defaultOneDriveDriveDetails[\"id\"].str));\n\t\t\t} else {\n\t\t\t\t// Use 'defaultOneDriveDriveDetails' as is for all other account types\n\t\t\t\tappConfig.defaultDriveId = defaultOneDriveDriveDetails[\"id\"].str;\n\t\t\t}\n\t\t\t\n\t\t\t// Make sure that appConfig.defaultDriveId is in our driveIDs array to use when checking if item is in database\n\t\t\t// Keep the DriveDetailsCache array with unique entries only\n\t\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\t\tif (!canFindDriveId(appConfig.defaultDriveId, cachedOnlineDriveData)) {\n\t\t\t\t// Add this driveId to the drive cache, which then also sets for the defaultDriveId:\n\t\t\t\t// - quotaRestricted;\n\t\t\t\t// - quotaAvailable;\n\t\t\t\t// - quotaRemaining;\n\t\t\t\t//\n\t\t\t\t// In some cases OneDrive Business configurations 'restrict' quota details thus is empty / blank / negative value / zero value\n\t\t\t\t// When addOrUpdateOneDriveOnlineDetails() is called, messaging is provided if these are zero, negative or missing (thus quota is being restricted)\n\t\t\t\taddOrUpdateOneDriveOnlineDetails(appConfig.defaultDriveId);\n\t\t\t}\n\t\t\t\n\t\t\t// Fetch the details from cachedOnlineDriveData for appConfig.defaultDriveId\n\t\t\tcachedOnlineDriveData = getDriveDetails(appConfig.defaultDriveId);\n\t\t\t// - cachedOnlineDriveData.quotaRestricted;\n\t\t\t// - cachedOnlineDriveData.quotaAvailable;\n\t\t\t// - cachedOnlineDriveData.quotaRemaining;\n\t\t\t\n\t\t\t// What did we set based on the data from the JSON and cached drive data\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"appConfig.accountType                 = \" ~ appConfig.accountType, [\"debug\"]);\n\t\t\t\taddLogEntry(\"appConfig.defaultDriveId              = \" ~ appConfig.defaultDriveId, [\"debug\"]);\n\t\t\t\taddLogEntry(\"cachedOnlineDriveData.quotaRemaining  = \" ~ to!string(cachedOnlineDriveData.quotaRemaining), [\"debug\"]);\n\t\t\t\taddLogEntry(\"cachedOnlineDriveData.quotaAvailable  = \" ~ to!string(cachedOnlineDriveData.quotaAvailable), [\"debug\"]);\n\t\t\t\taddLogEntry(\"cachedOnlineDriveData.quotaRestricted = \" ~ to!string(cachedOnlineDriveData.quotaRestricted), [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Regardless of this being all set - based on the JSON response, check for 'quota' being present, to check \n\t\t\t// for the following valid states: normal | nearing | critical | exceeded\n\t\t\t//\n\t\t\t// Based on this, then generate an applicable application message to advise the user of their quota status\n\t\t\tif ((hasQuota(defaultOneDriveDriveDetails)) && (hasQuotaState(defaultOneDriveDriveDetails))) {\n\t\t\t\t// get the current state\n\t\t\t\tstring quotaState = defaultOneDriveDriveDetails[\"quota\"][\"state\"].str;\n\t\t\t\t\n\t\t\t\t// quotaState = normal - no message\n\t\t\t\tstring nearingMessage = \"WARNING: Your Microsoft OneDrive storage is nearing capacity, with less than 10% of your available space remaining.\";\n\t\t\t\tstring criticalMessage = \"WARNING: Your Microsoft OneDrive storage is critically low, with less than 1% of your available space remaining.\";\n\t\t\t\tstring exceededMessage = \"CRITICAL: Your Microsoft OneDrive storage limit has been exceeded. You can no longer upload new content to Microsoft OneDrive.\";\n\t\t\t\tstring actionRequired = \"         Delete unneeded files or upgrade your storage plan now, as further uploads will not be possible once storage is exceeded\";\n\t\t\t\t\n\t\t\t\t// switch to display the right message\n\t\t\t\tswitch(quotaState) {\n\t\t\t\t\tcase \"nearing\":\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(nearingMessage, [\"info\", \"notify\"]);\n\t\t\t\t\t\taddLogEntry(actionRequired);\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"critical\":\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(criticalMessage, [\"info\", \"notify\"]);\n\t\t\t\t\t\taddLogEntry(actionRequired);\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"exceeded\":\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"******************************************************************************************************************************\");\n\t\t\t\t\t\taddLogEntry(exceededMessage, [\"info\", \"notify\"]);\n\t\t\t\t\t\taddLogEntry(\"******************************************************************************************************************************\");\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t// nothing\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Did the configuration file contain a 'drive_id' entry\n\t\t\t// If this exists, this will be a 'documentLibrary'\n\t\t\tif (appConfig.getValueString(\"drive_id\").length) {\n\t\t\t\t// Force set these as for whatever reason we could to query these via the getDefaultDriveDetails API call\n\t\t\t\tappConfig.accountType = \"documentLibrary\";\n\t\t\t\tappConfig.defaultDriveId = appConfig.getValueString(\"drive_id\");\n\t\t\t} else {\n\t\t\t\t// was this a no-sync task?\n\t\t\t\tif (!noSyncTask) {\n\t\t\t\t\t// Handle the invalid JSON response by throwing an exception error\n\t\t\t\t\tthrow new AccountDetailsException();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tgetDefaultDriveApiInstance.releaseCurlEngine();\n\t\tgetDefaultDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Get Default Root Details for this Account\n\tvoid getDefaultRootDetails() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Function variables\n\t\tJSONValue defaultOneDriveRootDetails;\n\t\tbool noSyncTask = false;\n\t\t\n\t\t// Create a new instance of the OneDrive API\n\t\tOneDriveApi getDefaultRootApiInstance;\n\t\tgetDefaultRootApiInstance = new OneDriveApi(appConfig);\n\t\tgetDefaultRootApiInstance.initialise();\n\t\t\n\t\t// Are we not doing a --sync or a --monitor operation? Both of these will be false if they are not set\n\t\tif ((!appConfig.getValueBool(\"synchronize\")) && (!appConfig.getValueBool(\"monitor\"))) {\n\t\t\t// set flag\n\t\t\tnoSyncTask = true;\n\t\t}\n\t\t\n\t\t// Get Default Root Details for this Account\n\t\ttry {\n\t\t\tif (debugLogging) {addLogEntry(\"Getting Account Default Root Details\", [\"debug\"]);}\n\t\t\tdefaultOneDriveRootDetails = getDefaultRootApiInstance.getDefaultRootDetails();\n\t\t} catch (OneDriveException exception) {\n\t\t\tif (debugLogging) {addLogEntry(\"defaultOneDriveRootDetails = getDefaultRootApiInstance.getDefaultRootDetails() generated a OneDriveException\", [\"debug\"]);}\n\t\t\t\n\t\t\tif ((exception.httpStatusCode == 400) || (exception.httpStatusCode == 401)) {\n\t\t\t\t// Handle the 400 | 401 error\n\t\t\t\thandleClientUnauthorised(exception.httpStatusCode, exception.error);\n\t\t\t} else {\n\t\t\t\t// Default operation if not 400,401 errors\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within getDefaultRootApiInstance\n\t\t\t\t// Display what the error is\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// If the JSON response is a correct JSON object, and has an 'id' we can set these details\n\t\tif ((defaultOneDriveRootDetails.type() == JSONType.object) && (hasId(defaultOneDriveRootDetails))) {\n\t\t\t// Read the returned JSON data for the root drive details\n\t\t\tif (debugLogging) {addLogEntry(\"OneDrive Account Default Root Details:       \" ~ sanitiseJSONItem(defaultOneDriveRootDetails), [\"debug\"]);}\n\t\t\tappConfig.defaultRootId = defaultOneDriveRootDetails[\"id\"].str;\n\t\t\tif (debugLogging) {addLogEntry(\"appConfig.defaultRootId      = \" ~ appConfig.defaultRootId, [\"debug\"]);}\n\t\t\t\n\t\t\t// Save the item to the database, so the account root drive is is always going to be present in the DB\n\t\t\tsaveItem(defaultOneDriveRootDetails);\n\t\t} else {\n\t\t\t// was this a no-sync task?\n\t\t\tif (!noSyncTask) {\n\t\t\t\t// Handle the invalid JSON response by throwing an exception error\n\t\t\t\tthrow new AccountDetailsException();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tgetDefaultRootApiInstance.releaseCurlEngine();\n\t\tgetDefaultRootApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Reset syncFailures to false based on file activity\n\tvoid resetSyncFailures() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Log initial status and any non-empty arrays\n\t\tstring logMessage = \"Evaluating reset of syncFailures: \";\n\t\tif (fileDownloadFailures.length > 0) {\n\t\t\tlogMessage ~= \"fileDownloadFailures is not empty; \";\n\t\t}\n\t\tif (fileUploadFailures.length > 0) {\n\t\t\tlogMessage ~= \"fileUploadFailures is not empty; \";\n\t\t}\n\n\t\t// Check if both arrays are empty to reset syncFailures\n\t\tif (fileDownloadFailures.length == 0 && fileUploadFailures.length == 0) {\n\t\t\tif (syncFailures) {\n\t\t\t\tsyncFailures = false;\n\t\t\t\tlogMessage ~= \"Resetting syncFailures to false.\";\n\t\t\t} else {\n\t\t\t\tlogMessage ~= \"syncFailures already false.\";\n\t\t\t}\n\t\t} else {\n\t\t\t// Indicate no reset of syncFailures due to non-empty conditions\n\t\t\tlogMessage ~= \"Not resetting syncFailures due to non-empty arrays.\";\n\t\t}\n\n\t\t// Log the final decision and conditions\n\t\tif (debugLogging) {addLogEntry(logMessage, [\"debug\"]);}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Perform a sync of the OneDrive Account\n\t// - Query /delta\n\t//\t\t- If singleDirectoryScope or nationalCloudDeployment is used we need to generate a /delta like response\n\t// - Process changes (add, changes, moves, deletes)\n\t// - Process any items to add (download data to local)\n\t// - Detail any files that we failed to download\n\t// - Process any deletes (remove local data)\n\tvoid syncOneDriveAccountToLocalDisk() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// performFullScanTrueUp value\n\t\tif (debugLogging) {addLogEntry(\"Perform a Full Scan True-Up: \" ~ to!string(appConfig.fullScanTrueUpRequired), [\"debug\"]);}\n\t\t\n\t\t// Fetch the API response of /delta to track changes that were performed online\n\t\tfetchOneDriveDeltaAPIResponse();\n\t\t\n\t\t// Process any download activities or cleanup actions\n\t\tprocessDownloadActivities();\n\t\t\n\t\t// If singleDirectoryScope is false, we are not targeting a single directory\n\t\t// but if true, the target 'could' be a shared folder - so dont try and scan it again\n\t\tif (!singleDirectoryScope) {\n\t\t\t// OneDrive Shared Folder Handling\n\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t// Personal Account Type\n\t\t\t\t// https://github.com/OneDrive/onedrive-api-docs/issues/764\n\t\t\t\t\n\t\t\t\t// Get the Remote Items from the Database\n\t\t\t\tItem[] remoteItems = itemDB.selectRemoteItems();\n\t\t\t\tforeach (remoteItem; remoteItems) {\n\t\t\t\t\t// Check if this path is specifically excluded by 'skip_dir', but only if 'skip_dir' is not empty\n\t\t\t\t\tif (appConfig.getValueString(\"skip_dir\") != \"\") {\n\t\t\t\t\t\t// The path that needs to be checked needs to include the '/'\n\t\t\t\t\t\t// This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched\n\t\t\t\t\t\tif (selectiveSync.isDirNameExcluded(remoteItem.name)) {\n\t\t\t\t\t\t\t// This directory name is excluded\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by skip_dir config: \" ~ remoteItem.name, [\"verbose\"]);}\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Directory name is not excluded or skip_dir is not populated\n\t\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\t\t// So that we represent correctly where this shared folder is, calculate the path\n\t\t\t\t\t\tstring sharedFolderLogicalPath = computeItemPath(remoteItem.driveId, remoteItem.id);\n\t\t\t\t\t\taddLogEntry(\"Syncing this OneDrive Personal Shared Folder: \" ~ ensureStartsWithDotSlash(sharedFolderLogicalPath));\n\t\t\t\t\t}\n\t\t\t\t\t// Check this OneDrive Personal Shared Folder for changes\n\t\t\t\t\tfetchOneDriveDeltaAPIResponse(remoteItem.remoteDriveId, remoteItem.remoteId, remoteItem.name);\n\t\t\t\t\t\n\t\t\t\t\t// Process any download activities or cleanup actions for this OneDrive Personal Shared Folder\n\t\t\t\t\tprocessDownloadActivities();\n\t\t\t\t}\n\t\t\t\t// Clear the array\n\t\t\t\tremoteItems = [];\n\t\t\t} else {\n\t\t\t\t// Is this a Business Account with Sync Business Shared Items enabled?\n\t\t\t\tif ((appConfig.accountType == \"business\") && (appConfig.getValueBool(\"sync_business_shared_items\"))) {\n\t\t\t\t\n\t\t\t\t\t// Business Account Shared Items Handling\n\t\t\t\t\t// - OneDrive Business Shared Folder\n\t\t\t\t\t// - OneDrive Business Shared Files\n\t\t\t\t\t// - SharePoint Links\n\t\t\t\t\n\t\t\t\t\t// Get the Remote Items from the Database\n\t\t\t\t\tItem[] remoteItems = itemDB.selectRemoteItems();\n\t\t\t\t\t\n\t\t\t\t\tforeach (remoteItem; remoteItems) {\n\t\t\t\t\t\t// As all remote items are returned, including files, we only want to process directories here\n\t\t\t\t\t\tif (remoteItem.remoteType == ItemType.dir) {\n\t\t\t\t\t\t\t// Check if this path is specifically excluded by 'skip_dir', but only if 'skip_dir' is not empty\n\t\t\t\t\t\t\tif (appConfig.getValueString(\"skip_dir\") != \"\") {\n\t\t\t\t\t\t\t\t// The path that needs to be checked needs to include the '/'\n\t\t\t\t\t\t\t\t// This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched\n\t\t\t\t\t\t\t\tif (selectiveSync.isDirNameExcluded(remoteItem.name)) {\n\t\t\t\t\t\t\t\t\t// This directory name is excluded\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by skip_dir config: \" ~ remoteItem.name, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Directory name is not excluded or skip_dir is not populated\n\t\t\t\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\t\t\t\t// So that we represent correctly where this shared folder is, calculate the path\n\t\t\t\t\t\t\t\tstring sharedFolderLogicalPath = computeItemPath(remoteItem.driveId, remoteItem.id);\n\t\t\t\t\t\t\t\taddLogEntry(\"Syncing this OneDrive Business Shared Folder: \" ~ sharedFolderLogicalPath);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Debug log output\n\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\taddLogEntry(\"Fetching /delta API response for:\", [\"debug\"]);\n\t\t\t\t\t\t\t\taddLogEntry(\"    remoteItem.remoteDriveId: \" ~ remoteItem.remoteDriveId, [\"debug\"]);\n\t\t\t\t\t\t\t\taddLogEntry(\"    remoteItem.remoteId:      \" ~ remoteItem.remoteId, [\"debug\"]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Check this OneDrive Business Shared Folder for changes\n\t\t\t\t\t\t\tfetchOneDriveDeltaAPIResponse(remoteItem.remoteDriveId, remoteItem.remoteId, remoteItem.name);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Process any download activities or cleanup actions for this OneDrive Business Shared Folder\n\t\t\t\t\t\t\tprocessDownloadActivities();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Clear the array\n\t\t\t\t\tremoteItems = [];\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive Business Shared File Handling - but only if this option is enabled\n\t\t\t\t\tif (appConfig.getValueBool(\"sync_business_shared_files\")) {\n\t\t\t\t\t\t// We need to create a 'new' local folder in the 'sync_dir' where these shared files & associated folder structure will reside\n\t\t\t\t\t\t// Whilst these files are synced locally, the entire folder structure will need to be excluded from syncing back to OneDrive\n\t\t\t\t\t\t// But file changes , *if any* , will need to be synced back to the original shared file location\n\t\t\t\t\t\t//  .\n\t\t\t\t\t\t//\t├── Files Shared With Me\t\t\t\t\t\t\t\t\t\t\t\t\t-> Directory should not be created online | Not Synced\n\t\t\t\t\t\t//\t│          └── Display Name (email address) (of Account who shared file)\t-> Directory should not be created online | Not Synced\n\t\t\t\t\t\t//\t│          │   └── shared file.ext \t\t\t\t\t\t\t\t\t\t\t-> File synced with original shared file location on remote drive\n\t\t\t\t\t\t//\t│          │   └── shared file.ext \t\t\t\t\t\t\t\t\t\t\t-> File synced with original shared file location on remote drive\n\t\t\t\t\t\t//\t│          │   └── ......\t\t\t \t\t\t\t\t\t\t\t\t\t-> File synced with original shared file location on remote drive\n\t\t\t\t\t\t//\t│          └── Display Name (email address) ...\n\t\t\t\t\t\t//\t│\t\t└── shared file.ext ....\t\t\t\t\t\t\t\t\t\t\t-> File synced with original shared file location on remote drive\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Does the Local Folder to store the OneDrive Business Shared Files exist?\n\t\t\t\t\t\tif (!exists(appConfig.configuredBusinessSharedFilesDirectoryName)) {\n\t\t\t\t\t\t\t// Folder does not exist locally and needs to be created\n\t\t\t\t\t\t\taddLogEntry(\"Creating the OneDrive Business Shared Files Local Directory: \" ~ appConfig.configuredBusinessSharedFilesDirectoryName);\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\t\t// Local folder does not exist, thus needs to be created\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t// Attempt path creation\n\t\t\t\t\t\t\t\t\tmkdirRecurse(appConfig.configuredBusinessSharedFilesDirectoryName);\n\t\t\t\t\t\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t\t\t\t\t\t// Creating the path failed\n\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Unable to create the OneDrive Business Shared Files Local Directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// As this will not be created online, generate a response so it can be saved to the database\n\t\t\t\t\t\t\tItem sharedFilesPath = makeItem(createFakeResponse(baseName(appConfig.configuredBusinessSharedFilesDirectoryName)));\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Add DB record to the local database\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Creating|Updating into local database a DB record for storing OneDrive Business Shared Files: \" ~ to!string(sharedFilesPath), [\"debug\"]);}\n\t\t\t\t\t\t\titemDB.upsert(sharedFilesPath);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Folder exists locally, is the folder in the database? \n\t\t\t\t\t\t\t// Query DB for this path\n\t\t\t\t\t\t\tItem dbRecord;\n\t\t\t\t\t\t\tif (!itemDB.selectByPath(baseName(appConfig.configuredBusinessSharedFilesDirectoryName), appConfig.defaultDriveId, dbRecord)) {\n\t\t\t\t\t\t\t\t// As this will not be created online, generate a response so it can be saved to the database\n\t\t\t\t\t\t\t\tItem sharedFilesPath = makeItem(createFakeResponse(baseName(appConfig.configuredBusinessSharedFilesDirectoryName)));\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Add DB record to the local database\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Creating|Updating into local database a DB record for storing OneDrive Business Shared Files: \" ~ to!string(sharedFilesPath), [\"debug\"]);}\n\t\t\t\t\t\t\t\titemDB.upsert(sharedFilesPath);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Query for OneDrive Business Shared Files\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Checking for any applicable OneDrive Business Shared Files which need to be synced locally\", [\"verbose\"]);}\n\t\t\t\t\t\tqueryBusinessSharedObjects();\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Download any OneDrive Business Shared Files\n\t\t\t\t\t\tprocessDownloadActivities();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Cleanup arrays when used in --monitor loops\n\tvoid cleanupArrays() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Debug what we are doing\n\t\tif (debugLogging) {addLogEntry(\"Cleaning up all internal arrays used when processing data\", [\"debug\"]);}\n\t\t\n\t\t// Multi Dimensional Arrays\n\t\tidsToDelete.length = 0;\n\t\tidsFaked.length = 0;\n\t\tdatabaseItemsWhereContentHasChanged.length = 0;\n\t\t\n\t\t// JSON Items Arrays\n\t\tjsonItemsToProcess = [];\n\t\tfileJSONItemsToDownload = [];\n\t\tjsonItemsToResumeUpload = [];\n\t\tjsonItemsToResumeDownload = [];\n\t\t\n\t\t// String Arrays\n\t\tfileDownloadFailures = [];\n\t\tpathFakeDeletedArray = [];\n\t\tpathsRenamed = [];\n\t\tnewLocalFilesToUploadToOneDrive = [];\n\t\tfileUploadFailures = [];\n\t\tposixViolationPaths = [];\n\t\tbusinessSharedFoldersOnlineToSkip = [];\n\t\tinterruptedUploadsSessionFiles = [];\n\t\tinterruptedDownloadFiles = [];\n\t\tpathsToCreateOnline = [];\n\t\tdatabaseItemsToDeleteOnline = [];\n\t\tpathsRetained = [];\n\t\t\n\t\t// Perform Garbage Collection on this destroyed curl engine\n\t\tGC.collect();\n\t\tif (debugLogging) {addLogEntry(\"Cleaning of internal arrays complete\", [\"debug\"]);}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Configure singleDirectoryScope = true if this function is called\n\t// By default, singleDirectoryScope = false\n\tvoid setSingleDirectoryScope(string normalisedSingleDirectoryPath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Function variables\n\t\tItem searchItem;\n\t\tJSONValue onlinePathData;\n\t\t\n\t\t// Set the main flag\n\t\tsingleDirectoryScope = true;\n\t\t\n\t\t// What are we doing?\n\t\taddLogEntry(\"The OneDrive Client was asked to search for this directory online and create it if it's not located: \" ~ normalisedSingleDirectoryPath);\n\t\t\n\t\t// Query the OneDrive API for the specified path online\n\t\t// In a --single-directory scenario, we need to traverse the entire path that we are wanting to sync\n\t\t// 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\n\t\t// 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 \n\t\t// any object items from that location\n\t\t// This is because, in a --single-directory scenario, any folder in the entire path tree could be a 'case-insensitive match'\n\t\t\n\t\ttry {\n\t\t\tonlinePathData = queryOneDriveForSpecificPathAndCreateIfMissing(normalisedSingleDirectoryPath, true);\n\t\t} catch (PosixException e) {\n\t\t\tdisplayPosixErrorMessage(e.msg);\n\t\t\taddLogEntry(\"ERROR: Requested directory to search for and potentially create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online.\");\n\t\t}\n\t\t\n\t\t// Was a valid JSON response provided?\n\t\tif (onlinePathData.type() == JSONType.object) {\n\t\t\t// Valid JSON item was returned\n\t\t\tsearchItem = makeItem(onlinePathData);\n\t\t\tif (debugLogging) {addLogEntry(\"searchItem: \" ~ to!string(searchItem), [\"debug\"]);}\n\t\t\t\n\t\t\t// Is this item a potential Shared Folder?\n\t\t\t// Is this JSON a remote object\n\t\t\tif (isItemRemote(onlinePathData)) {\n\t\t\t\t// Is this a Personal Account Type or has 'sync_business_shared_items' been enabled?\n\t\t\t\tif ((appConfig.accountType == \"personal\") || (appConfig.getValueBool(\"sync_business_shared_items\"))) {\n\t\t\t\t\t// The path we are seeking is remote to our account drive id\n\t\t\t\t\tsearchItem.driveId = onlinePathData[\"remoteItem\"][\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tsearchItem.id = onlinePathData[\"remoteItem\"][\"id\"].str;\n\t\t\t\t\t\n\t\t\t\t\t// Issue #3115 - Validate driveId length\n\t\t\t\t\t// What account type is this?\n\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\t\t\tsearchItem.driveId = transformToLowerCase(searchItem.driveId);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\t\t\t\tif (searchItem.driveId != appConfig.defaultDriveId) {\n\t\t\t\t\t\t\tsearchItem.driveId = testProvidedDriveIdForLengthIssue(searchItem.driveId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(onlinePathData);\n\t\t\t\t} else {\n\t\t\t\t\t// This is a shared folder location, but we are not a 'personal' account, and 'sync_business_shared_items' has not been enabled\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\taddLogEntry(\"ERROR: The requested --single-directory path to sync is a Shared Folder online and 'sync_business_shared_items' is not enabled\");\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\tforceExit();\n\t\t\t\t}\n\t\t\t} \n\t\t\t\n\t\t\t// Set these items so that these can be used as required\n\t\t\tsingleDirectoryScopeDriveId = searchItem.driveId;\n\t\t\tsingleDirectoryScopeItemId = searchItem.id;\n\t\t} else {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"ERROR: The requested --single-directory path to sync has generated an error. Please correct this error and try again.\");\n\t\t\taddLogEntry();\n\t\t\tforceExit();\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Query OneDrive API for /delta changes and iterate through items online\n\tvoid fetchOneDriveDeltaAPIResponse(string driveIdToQuery = null, string itemIdToQuery = null, string sharedFolderName = null) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\t\n\t\tstring deltaLink = null;\n\t\tstring currentDeltaLink = null;\n\t\tstring databaseDeltaLink;\n\t\tJSONValue deltaChanges;\n\t\tlong responseBundleCount;\n\t\tlong jsonItemsReceived = 0;\n\t\t\n\t\t// Reset jsonItemsToProcess & processedCount\n\t\tjsonItemsToProcess = [];\n\t\tprocessedCount = 0;\n\t\t\n\t\t// Reset generateSimulatedDeltaResponse\n\t\tgenerateSimulatedDeltaResponse = false;\n\t\t\n\t\t// Reset Shared Folder Flags for 'sync_list' processing\n\t\tsharedFolderDeltaGeneration = false;\n\t\tcurrentSharedFolderName = \"\";\n\t\t\n\t\t// Was a driveId provided as an input\n\t\tif (strip(driveIdToQuery).empty) {\n\t\t\t// No provided driveId to query, use the account default\n\t\t\tdriveIdToQuery = appConfig.defaultDriveId;\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"driveIdToQuery was empty, setting to appConfig.defaultDriveId\", [\"debug\"]);\n\t\t\t\taddLogEntry(\"driveIdToQuery: \" ~ driveIdToQuery, [\"debug\"]);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Was an itemId provided as an input\n\t\tif (strip(itemIdToQuery).empty) {\n\t\t\t// No provided itemId to query, use the account default\n\t\t\titemIdToQuery = appConfig.defaultRootId;\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"itemIdToQuery was empty, setting to appConfig.defaultRootId\", [\"debug\"]);\n\t\t\t\taddLogEntry(\"itemIdToQuery: \" ~ itemIdToQuery, [\"debug\"]);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// What OneDrive API query do we use?\n\t\t// - Are we running against a National Cloud Deployments that does not support /delta ?\n\t\t//   National Cloud Deployments do not support /delta as a query\n\t\t//   https://docs.microsoft.com/en-us/graph/deployments#supported-features\n\t\t//\n\t\t// - Are we performing a --single-directory sync, which will exclude many items online, focusing in on a specific online directory\n\t\t// \n\t\t// - Are we performing a --download-only --cleanup-local-files action?\n\t\t//   - If we are, and we use a normal /delta query, we get all the local 'deleted' objects as well.\n\t\t//   - 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 .. \n\t\t//     the net effect of this, is that the valid local files we want to keep, are actually deleted ...... not desirable\n\t\tif ((singleDirectoryScope) || (nationalCloudDeployment) || (cleanupLocalFiles)) {\n\t\t\t// Generate a simulated /delta response so that we correctly capture the current online state, less any 'online' delete and replace activity\n\t\t\tgenerateSimulatedDeltaResponse = true;\n\t\t}\n\t\t\n\t\t// 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:\n\t\t//    Syncing this OneDrive Business Shared Folder: Sub Folder 2\n\t\t//    Fetching /delta response from the OneDrive API for Drive ID: b!fZgJhK-pU0eTQpylvmoYCkE4YgH_KRNDlxjRx9OWNqmV9Q_E_uWdRJKIB5L_ruPN\n\t\t//    Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 18\n\t\t//    Skipping path - excluded by sync_list config: Sub Folder Share/Sub Folder 1/Sub Folder 2\n\t\t//\n\t\t// 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\n\t\tif (!sharedFolderName.empty) {\n\t\t\t// When using 'sync_list' we need to do this\n\t\t\tsharedFolderDeltaGeneration = true;\n\t\t\tcurrentSharedFolderName = sharedFolderName;\n\t\t\tgenerateSimulatedDeltaResponse = true;\n\t\t}\n\t\t\n\t\t// Reset latestDeltaLink & deltaLinkCache\n\t\tlatestDeltaLink = null;\n\t\tdeltaLinkCache.driveId = null;\n\t\tdeltaLinkCache.itemId = null;\n\t\tdeltaLinkCache.latestDeltaLink = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\t\t\n\t\t// What /delta query do we use?\n\t\tif (!generateSimulatedDeltaResponse) {\n\t\t\t// This should be the majority default pathway application use\n\t\t\t\n\t\t\t// Do we need to perform a Full Scan True Up? Is 'appConfig.fullScanTrueUpRequired' set to 'true'?\n\t\t\tif (appConfig.fullScanTrueUpRequired) {\n\t\t\t\taddLogEntry(\"Performing a full scan of online data to ensure consistent local state\");\n\t\t\t\tif (debugLogging) {addLogEntry(\"Setting currentDeltaLink = null\", [\"debug\"]);}\n\t\t\t\tcurrentDeltaLink = null;\n\t\t\t} else {\n\t\t\t\t// Try and get the current Delta Link from the internal cache, this saves a DB I/O call\n\t\t\t\tcurrentDeltaLink = getDeltaLinkFromCache(deltaLinkInfo, driveIdToQuery);\n\t\t\t\t\n\t\t\t\t// Is currentDeltaLink empty (no cached entry found) ?\n\t\t\t\tif (currentDeltaLink.empty) {\n\t\t\t\t\t// Try and get the current delta link from the database for this DriveID and RootID\n\t\t\t\t\tdatabaseDeltaLink = itemDB.getDeltaLink(driveIdToQuery, itemIdToQuery);\n\t\t\t\t\tif (!databaseDeltaLink.empty) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Using database stored deltaLink\", [\"debug\"]);}\n\t\t\t\t\t\tcurrentDeltaLink = databaseDeltaLink;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Zero deltaLink available for use, we will be performing a full online scan\", [\"debug\"]);}\n\t\t\t\t\t\tcurrentDeltaLink = null;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Log that we are using the deltaLink for cache\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Using cached deltaLink\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Dynamic output for non-verbose and verbose run so that the user knows something is being retrieved from the OneDrive API\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\taddProcessingLogHeaderEntry(\"Fetching items from the OneDrive API for Drive ID: \" ~ driveIdToQuery, appConfig.verbosityCount);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Fetching /delta response from the OneDrive API for Drive ID: \" ~  driveIdToQuery, [\"verbose\"]);}\n\t\t\t}\n\t\t\t\n\t\t\t// Create a new API Instance for querying the actual /delta and initialise it\n\t\t\tOneDriveApi getDeltaDataOneDriveApiInstance;\n\t\t\tgetDeltaDataOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t\tgetDeltaDataOneDriveApiInstance.initialise();\n\n\t\t\t// Get the /delta changes via the OneDrive API\n\t\t\twhile (true) {\n\t\t\t\t// Check if exitHandlerTriggered is true\n\t\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Increment responseBundleCount\n\t\t\t\tresponseBundleCount++;\n\t\t\t\t\n\t\t\t\t// Ensure deltaChanges is empty before we query /delta\n\t\t\t\tdeltaChanges = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// getDeltaChangesByItemId has the re-try logic for transient errors\n\t\t\t\tdeltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, currentDeltaLink, getDeltaDataOneDriveApiInstance);\n\t\t\t\t\n\t\t\t\t// If the initial deltaChanges response is an invalid JSON object, keep trying until we get a valid response ..\n\t\t\t\tif (deltaChanges.type() != JSONType.object) {\n\t\t\t\t\t// While the response is not a JSON Object or the Exit Handler has not been triggered\n\t\t\t\t\twhile (deltaChanges.type() != JSONType.object) {\n\t\t\t\t\t\t// Check if exitHandlerTriggered is true\n\t\t\t\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t\t// Handle the invalid JSON response and retry\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"ERROR: Query of the OneDrive API via deltaChanges = getDeltaChangesByItemId() returned an invalid JSON response\", [\"debug\"]);}\n\t\t\t\t\t\tdeltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, currentDeltaLink, getDeltaDataOneDriveApiInstance);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tlong nrChanges = count(deltaChanges[\"value\"].array);\n\t\t\t\tint changeCount = 0;\n\t\t\t\t\n\t\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t\t// Dynamic output for a non-verbose run so that the user knows something is happening\n\t\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\t\taddProcessingDotEntry();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Processing API Response Bundle: \" ~ to!string(responseBundleCount) ~ \" - Quantity of 'changes|items' in this bundle to process: \" ~ to!string(nrChanges), [\"verbose\"]);}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Update the count of items received\n\t\t\t\tjsonItemsReceived = jsonItemsReceived + nrChanges;\n\t\t\t\t\n\t\t\t\t// The 'deltaChanges' response may contain either @odata.nextLink or @odata.deltaLink\n\t\t\t\t// Check for @odata.nextLink\n\t\t\t\tif (\"@odata.nextLink\" in deltaChanges) {\n\t\t\t\t\t// @odata.nextLink is the pointer within the API to the next '200+' JSON bundle - this is the checkpoint link for this bundle\n\t\t\t\t\t// This URL changes between JSON bundle sets\n\t\t\t\t\t// Log the action of setting currentDeltaLink to @odata.nextLink\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting currentDeltaLink to @odata.nextLink: \" ~ deltaChanges[\"@odata.nextLink\"].str, [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Update currentDeltaLink to @odata.nextLink for the next '200+' JSON bundle - this is the checkpoint link for this bundle\n\t\t\t\t\tcurrentDeltaLink = deltaChanges[\"@odata.nextLink\"].str;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Check for @odata.deltaLink - usually only in the LAST JSON changeset bundle\n\t\t\t\tif (\"@odata.deltaLink\" in deltaChanges) {\n\t\t\t\t\t// @odata.deltaLink is the pointer that finalises all the online 'changes' for this particular checkpoint\n\t\t\t\t\t// When the API is queried again, this is fetched from the DB as this is the starting point\n\t\t\t\t\t// The API issue here is - the LAST JSON bundle will ONLY ever contain this item, meaning if this is then committed to the database\n\t\t\t\t\t// 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\n\t\t\t\t\t// This is an API capability gap:\n\t\t\t\t\t//\n\t\t\t\t\t// ..\n\t\t\t\t\t// @odata.nextLink:  https://graph.microsoft.com/v1.0/drives/<redacted>/items/<redacted>/delta?token=<redacted>\n\t\t\t\t\t// Processing API Response Bundle: 115 - Quantity of 'changes|items' in this bundle to process: 204\n\t\t\t\t\t// ..\n\t\t\t\t\t// @odata.nextLink:  https://graph.microsoft.com/v1.0/drives/<redacted>/items/<redacted>/delta?token=<redacted>\n\t\t\t\t\t// Processing API Response Bundle: 127 - Quantity of 'changes|items' in this bundle to process: 204\n\t\t\t\t\t// @odata.nextLink:  https://graph.microsoft.com/v1.0/drives/<redacted>/items/<redacted>/delta?token=<redacted>\n\t\t\t\t\t// Processing API Response Bundle: 128 - Quantity of 'changes|items' in this bundle to process: 176\n\t\t\t\t\t// @odata.deltaLink: https://graph.microsoft.com/v1.0/drives/<redacted>/items/<redacted>/delta?token=<redacted>\n\t\t\t\t\t// Finished processing /delta JSON response from the OneDrive API\n\t\t\t\t\t\n\t\t\t\t\t// Log the action of setting currentDeltaLink to @odata.deltaLink\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting currentDeltaLink to (@odata.deltaLink): \" ~ deltaChanges[\"@odata.deltaLink\"].str, [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Update currentDeltaLink to @odata.deltaLink as the final checkpoint URL for this entire JSON response set\n\t\t\t\t\tcurrentDeltaLink = deltaChanges[\"@odata.deltaLink\"].str;\n\t\t\t\t\t\n\t\t\t\t\t// Store this currentDeltaLink as latestDeltaLink\n\t\t\t\t\tlatestDeltaLink = deltaChanges[\"@odata.deltaLink\"].str;\n\t\t\t\t\t\n\t\t\t\t\t// Issue #3115 - Validate driveId length\n\t\t\t\t\t// What account type is this?\n\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\t\t\tdriveIdToQuery = transformToLowerCase(driveIdToQuery);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\t\t\t\tif (driveIdToQuery != appConfig.defaultDriveId) {\n\t\t\t\t\t\t\tdriveIdToQuery = testProvidedDriveIdForLengthIssue(driveIdToQuery);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Update deltaLinkCache\n\t\t\t\t\tdeltaLinkCache.driveId = driveIdToQuery;\n\t\t\t\t\tdeltaLinkCache.itemId = itemIdToQuery;\n\t\t\t\t\tdeltaLinkCache.latestDeltaLink = currentDeltaLink;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// We have a valid deltaChanges JSON array. This means we have at least 200+ JSON items to process.\n\t\t\t\t// 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\n\t\t\t\tauto jsonArrayToProcess = deltaChanges[\"value\"].array;\n\t\t\t\t\n\t\t\t\t// To allow for better debugging, what are all the JSON elements in the array the API responded with in this set?\n\t\t\t\tif (count(jsonArrayToProcess) > 0) {\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\tstring debugLogHeader = format(\"=============================== jsonArrayToProcess - response bundle %s ===================================\", to!string(responseBundleCount));\n\t\t\t\t\t\taddLogEntry(debugLogHeader, [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(to!string(jsonArrayToProcess), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(debugLogBreakType2, [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Process the change set\n\t\t\t\tforeach (onedriveJSONItem; jsonArrayToProcess) {\n\t\t\t\t\t// increment change count for this item\n\t\t\t\t\tchangeCount++;\n\t\t\t\t\t// Process the received OneDrive object item JSON for this JSON bundle\n\t\t\t\t\t// This will determine its initial applicability and perform some initial processing on the JSON if required\n\t\t\t\t\tprocessDeltaJSONItem(onedriveJSONItem, nrChanges, changeCount, responseBundleCount, singleDirectoryScope);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Clear up this data\n\t\t\t\tjsonArrayToProcess = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Is latestDeltaLink matching deltaChanges[\"@odata.deltaLink\"].str ?\n\t\t\t\tif (\"@odata.deltaLink\" in deltaChanges) {\n\t\t\t\t\tif (latestDeltaLink == deltaChanges[\"@odata.deltaLink\"].str) {\n\t\t\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Cleanup deltaChanges as this is no longer needed\n\t\t\t\tdeltaChanges = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t\t}\n\t\t\t\n\t\t\t// Terminate getDeltaDataOneDriveApiInstance here\n\t\t\tgetDeltaDataOneDriveApiInstance.releaseCurlEngine();\n\t\t\tgetDeltaDataOneDriveApiInstance = null;\n\t\t\t// Perform Garbage Collection on this destroyed curl engine\n\t\t\tGC.collect();\n\t\t\t\n\t\t\t// To finish off the JSON processing items, this is needed to reflect this in the log\n\t\t\tif (debugLogging) {addLogEntry(debugLogBreakType1, [\"debug\"]);}\n\t\t\t\n\t\t\t// Log that we have finished querying the /delta API\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\t// Close out the '....' being printed to the console\n\t\t\t\t\tcompleteProcessingDots();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Finished processing /delta JSON response from the OneDrive API\", [\"verbose\"]);}\n\t\t\t}\n\t\t\t\n\t\t\t// 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\n\t\t\tif (appConfig.fullScanTrueUpRequired) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Unsetting fullScanTrueUpRequired as this has been performed\", [\"debug\"]);}\n\t\t\t\tappConfig.fullScanTrueUpRequired = false;\n\t\t\t}\n\t\t\t\n\t\t\t// Cleanup deltaChanges as this is no longer needed\n\t\t\tdeltaChanges = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t} else {\n\t\t\t// Why are we generating a /delta response\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"Why are we generating a /delta response:\", [\"debug\"]);\n\t\t\t\taddLogEntry(\" singleDirectoryScope:    \" ~ to!string(singleDirectoryScope), [\"debug\"]);\n\t\t\t\taddLogEntry(\" nationalCloudDeployment: \" ~ to!string(nationalCloudDeployment), [\"debug\"]);\n\t\t\t\taddLogEntry(\" cleanupLocalFiles:       \" ~ to!string(cleanupLocalFiles), [\"debug\"]);\n\t\t\t\taddLogEntry(\" sharedFolderName:        \" ~ sharedFolderName, [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// What 'path' are we going to start generating the response for\n\t\t\tstring pathToQuery;\n\t\t\t\n\t\t\t// If --single-directory has been called, use the value that has been set\n\t\t\tif (singleDirectoryScope) {\n\t\t\t\tpathToQuery = appConfig.getValueString(\"single_directory\");\n\t\t\t}\n\t\t\t\n\t\t\t// We could also be syncing a Shared Folder of some description - is this empty?\n\t\t\tif (!sharedFolderName.empty) {\n\t\t\t\t// We need to build 'pathToQuery' to support Shared Folders being anywhere in the directory structure (#2824)\n\t\t\t\t// Is the itemIdToQuery in the database? If this is not there, we cannot build the path\n\t\t\t\tif (itemDB.idInLocalDatabase(driveIdToQuery, itemIdToQuery)) {\n\t\t\t\t\t// 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\n\t\t\t\t\tItem remoteEntryItem;\n\t\t\t\t\titemDB.selectByRemoteEntryByName(sharedFolderName, remoteEntryItem);\n\t\t\t\t\t\n\t\t\t\t\t// 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\n\t\t\t\t\tstring computedLocalPathToQuery = computeItemPath(remoteEntryItem.driveId, remoteEntryItem.id);\n\t\t\t\t\t// If we have a computed path, use it, else use 'sharedFolderName'\n\t\t\t\t\tif (!computedLocalPathToQuery.empty) {\n\t\t\t\t\t\t// computedLocalPathToQuery is not empty\n\t\t\t\t\t\tpathToQuery = computedLocalPathToQuery;\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// computedLocalPathToQuery is empty\n\t\t\t\t\t\tpathToQuery = sharedFolderName;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// shared folder details are not even in the database ... fall back to this\n\t\t\t\t\tpathToQuery = sharedFolderName;\n\t\t\t\t}\n\t\t\t\t// 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\n\t\t\t}\n\t\t\t\n\t\t\t// Generate the simulated /delta response\n\t\t\t//\n\t\t\t// 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\n\t\t\t// 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 \n\t\t\t// the returned JSON items from the API, we flag the item as back in sync, then we can cleanup any out-of-sync items\n\t\t\t//\n\t\t\t// The flagging of the local database items to 'N' is handled within the generateDeltaResponse() function\n\t\t\t//\n\t\t\t// 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' \n\t\t\t// This then allows the application to look for any remaining 'N' values, and delete these as no longer needed locally\n\t\t\tdeltaChanges = generateDeltaResponse(pathToQuery);\n\t\t\t\n\t\t\t// deltaChanges must be a valid JSON object / array of data\n\t\t\tif (deltaChanges.type() == JSONType.object) {\n\t\t\t\t// How many changes were returned?\n\t\t\t\tlong nrChanges = count(deltaChanges[\"value\"].array);\n\t\t\t\tint changeCount = 0;\n\t\t\t\tif (debugLogging) {addLogEntry(\"API Response Bundle: \" ~ to!string(responseBundleCount) ~ \" - Quantity of 'changes|items' in this bundle to process: \" ~ to!string(nrChanges), [\"debug\"]);}\n\t\t\t\t// Update the count of items received\n\t\t\t\tjsonItemsReceived = jsonItemsReceived + nrChanges;\n\t\t\t\t\n\t\t\t\t// 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\n\t\t\t\tauto jsonArrayToProcess = deltaChanges[\"value\"].array;\n\t\t\t\tforeach (onedriveJSONItem; deltaChanges[\"value\"].array) {\n\t\t\t\t\t// increment change count for this item\n\t\t\t\t\tchangeCount++;\n\t\t\t\t\t// Process the received OneDrive object item JSON for this JSON bundle\n\t\t\t\t\t// When we generate a /delta response .. there is no currentDeltaLink value\n\t\t\t\t\tprocessDeltaJSONItem(onedriveJSONItem, nrChanges, changeCount, responseBundleCount, singleDirectoryScope);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Clear up this data\n\t\t\t\tjsonArrayToProcess = null;\n\t\t\t\t\n\t\t\t\t// To finish off the JSON processing items, this is needed to reflect this in the log\n\t\t\t\tif (debugLogging) {addLogEntry(debugLogBreakType1, [\"debug\"]);}\n\t\t\t\n\t\t\t\t// Log that we have finished generating our self generated /delta response\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\taddLogEntry(\"Finished processing self generated /delta JSON response from the OneDrive API\");\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Cleanup deltaChanges as this is no longer needed\n\t\t\tdeltaChanges = null;\n\t\t\t\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t}\n\t\t\n\t\t// Cleanup deltaChanges as this is no longer needed\n\t\tdeltaChanges = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// We have JSON items received from the OneDrive API\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Number of JSON Objects received from OneDrive API:                 \" ~ to!string(jsonItemsReceived), [\"debug\"]);\n\t\t\taddLogEntry(\"Number of JSON Objects already processed (root and deleted items): \" ~ to!string((jsonItemsReceived - jsonItemsToProcess.length)), [\"debug\"]);\n\t\t\t// We should have now at least processed all the JSON items as returned by the /delta call\n\t\t\t// 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\n\t\t\taddLogEntry(\"Number of JSON items submitted for further processing is: \" ~ to!string(jsonItemsToProcess.length), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Are there items to process?\n\t\tif (jsonItemsToProcess.length > 0) {\n\t\t\t// Lets deal with the JSON items in a batch process\n\t\t\tsize_t batchSize = 500;\n\t\t\tlong batchCount = (jsonItemsToProcess.length + batchSize - 1) / batchSize;\n\t\t\tlong batchesProcessed = 0;\n\t\t\t\n\t\t\t// Dynamic output for a non-verbose run so that the user knows something is happening\n\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\taddProcessingLogHeaderEntry(\"Processing \" ~ to!string(jsonItemsToProcess.length) ~ \" applicable JSON items received from Microsoft OneDrive\", appConfig.verbosityCount);\n\t\t\t}\n\t\t\t\n\t\t\t// For each batch, process the JSON items that need to be now processed.\n\t\t\t// 'root' and deleted objects have already been handled\n\t\t\tforeach (batchOfJSONItems; jsonItemsToProcess.chunks(batchSize)) {\n\t\t\t\t// Chunk the total items to process into 500 lot items\n\t\t\t\tbatchesProcessed++;\n\t\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t\t// Dynamic output for a non-verbose run so that the user knows something is happening\n\t\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\t\taddProcessingDotEntry();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Processing OneDrive JSON item batch [\" ~ to!string(batchesProcessed) ~ \"/\" ~ to!string(batchCount) ~ \"] to ensure consistent local state\", [\"verbose\"]);}\n\t\t\t\t}\t\n\t\t\t\t\t\n\t\t\t\t// Process the batch\n\t\t\t\tprocessJSONItemsInBatch(batchOfJSONItems, batchesProcessed, batchCount);\n\t\t\t\t\n\t\t\t\t// To finish off the JSON processing items, this is needed to reflect this in the log\n\t\t\t\tif (debugLogging) {addLogEntry(debugLogBreakType1, [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// For this set of items, perform a DB PASSIVE checkpoint\n\t\t\t\titemDB.performCheckpoint(\"PASSIVE\");\n\t\t\t}\n\t\t\t\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t// close off '.' output\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\t// Close out the '....' being printed to the console\n\t\t\t\t\tcompleteProcessingDots();\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Debug output - what was processed\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"Number of JSON items to process is: \" ~ to!string(jsonItemsToProcess.length), [\"debug\"]);\n\t\t\t\taddLogEntry(\"Number of JSON items processed was: \" ~ to!string(processedCount), [\"debug\"]);\n\t\t\t\taddLogEntry(\"\", [\"debug\"]);\n\t\t\t\tstring jsonProcessingCompleteLineEntry = format(\"Processing of JSON items from driveId %s and itemId %s is complete\", driveIdToQuery, itemIdToQuery);\n\t\t\t\taddLogEntry(jsonProcessingCompleteLineEntry, [\"debug\"]);\n\t\t\t\taddLogEntry(\"\", [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Notification to user regarding number of objects received from OneDrive API\n\t\t\tif (jsonItemsReceived >= 300000) {\n\t\t\t\t// 'driveIdToQuery' should be the drive where the JSON responses came from\n\t\t\t\tstring objectsExceedLimitWarning = format(\"WARNING: The number of objects stored online in '%s' exceeds Microsoft OneDrive's recommended limit. This may cause unreliable application behaviour due to inconsistent or incomplete API responses. Immediate action is strongly advised to avoid data integrity issues.\", driveIdToQuery);\n\t\t\t\taddLogEntry(objectsExceedLimitWarning, [\"info\", \"notify\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Free up memory and items processed as it is pointless now having this data around\n\t\t\tjsonItemsToProcess = [];\n\t\t\t\n\t\t\t// Perform Garbage Collection on this destroyed curl engine\n\t\t\tGC.collect();\n\t\t} else {\n\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\taddLogEntry(\"No changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive\");\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Keep the DriveDetailsCache array with unique entries only\n\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\tif (!canFindDriveId(driveIdToQuery, cachedOnlineDriveData)) {\n\t\t\t// Add this driveId to the drive cache\n\t\t\taddOrUpdateOneDriveOnlineDetails(driveIdToQuery);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Process the /delta API JSON response items\n\tvoid processDeltaJSONItem(JSONValue onedriveJSONItem, long nrChanges, int changeCount, long responseBundleCount, bool singleDirectoryScope) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Variables for this JSON item\n\t\tstring thisItemId;\n\t\tbool itemIsRoot = false;\n\t\tbool handleItemAsRootObject = false;\n\t\tbool itemIsDeletedOnline = false;\n\t\tbool itemHasParentReferenceId = false;\n\t\tbool itemHasParentReferencePath = false;\n\t\tbool itemIdMatchesDefaultRootId = false;\n\t\tbool itemNameExplicitMatchRoot = false;\n\t\tbool itemIsRemoteItem = false;\n\t\tstring objectParentDriveId;\n\t\tstring objectParentId;\n\t\tMonoTime jsonProcessingStartTime;\n\t\t\n\t\t// Debugging the processing start of the JSON item\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(debugLogBreakType1, [\"debug\"]);\n\t\t\tjsonProcessingStartTime = MonoTime.currTime();\n\t\t\taddLogEntry(\"Processing OneDrive Item \" ~ to!string(changeCount) ~ \" of \" ~ to!string(nrChanges) ~ \" from API Response Bundle \" ~ to!string(responseBundleCount), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Issue #3336 - Convert driveId to lowercase\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// We must massage this raw JSON record to force the onedriveJSONItem[\"parentReference\"][\"driveId\"] to lowercase\n\t\t\tif (hasParentReferenceDriveId(onedriveJSONItem)) {\n\t\t\t\t// This JSON record has a driveId we now must manipulate to lowercase\n\t\t\t\tstring originalDriveIdValue = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\tonedriveJSONItem[\"parentReference\"][\"driveId\"] = transformToLowerCase(originalDriveIdValue);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Debug output of the raw JSON item we are processing\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Raw JSON OneDrive Item: \" ~ sanitiseJSONItem(onedriveJSONItem), [\"debug\"]);\n\t\t}\n\t\t\t\t\n\t\t// What is this item's id\n\t\tthisItemId = onedriveJSONItem[\"id\"].str;\n\t\t\n\t\t// Is this a deleted item - only calculate this once\n\t\titemIsDeletedOnline = isItemDeleted(onedriveJSONItem);\n\t\tif (!itemIsDeletedOnline) {\n\t\t\t// This is not a deleted item\n\t\t\tif (debugLogging) {addLogEntry(\"This item is not a OneDrive online deletion change\", [\"debug\"]);}\n\t\t\t\n\t\t\t// Only calculate these elements once\n\t\t\titemIsRoot = isItemRoot(onedriveJSONItem);\n\t\t\titemHasParentReferenceId = hasParentReferenceId(onedriveJSONItem);\n\t\t\titemIdMatchesDefaultRootId = (thisItemId == appConfig.defaultRootId);\n\t\t\titemNameExplicitMatchRoot = (onedriveJSONItem[\"name\"].str == \"root\");\n\t\t\tobjectParentDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\tif (itemHasParentReferenceId) {\n\t\t\t\tobjectParentId = onedriveJSONItem[\"parentReference\"][\"id\"].str;\n\t\t\t}\n\t\t\titemIsRemoteItem  = isItemRemote(onedriveJSONItem);\n\t\t\t\n\t\t\t// Test is this is the OneDrive Users Root?\n\t\t\t// Debug output of change evaluation items\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"defaultRootId                                        = \" ~ appConfig.defaultRootId, [\"debug\"]);\n\t\t\t\taddLogEntry(\"thisItemName                                         = \" ~ onedriveJSONItem[\"name\"].str, [\"debug\"]);\n\t\t\t\taddLogEntry(\"thisItemId                                           = \" ~ thisItemId, [\"debug\"]);\n\t\t\t\taddLogEntry(\"thisItemId == defaultRootId                          = \" ~ to!string(itemIdMatchesDefaultRootId), [\"debug\"]);\n\t\t\t\taddLogEntry(\"isItemRoot(onedriveJSONItem)                         = \" ~ to!string(itemIsRoot), [\"debug\"]);\n\t\t\t\taddLogEntry(\"onedriveJSONItem['name'].str == 'root'               = \" ~ to!string(itemNameExplicitMatchRoot), [\"debug\"]);\n\t\t\t\taddLogEntry(\"itemHasParentReferenceId                             = \" ~ to!string(itemHasParentReferenceId), [\"debug\"]);\n\t\t\t\taddLogEntry(\"itemIsRemoteItem                                     = \" ~ to!string(itemIsRemoteItem), [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\tif ( (itemIdMatchesDefaultRootId || singleDirectoryScope) && itemIsRoot && itemNameExplicitMatchRoot) {\n\t\t\t\t// This IS a OneDrive Root item or should be classified as such in the case of 'singleDirectoryScope'\n\t\t\t\tif (debugLogging) {addLogEntry(\"JSON item will flagged as a 'root' item\", [\"debug\"]);}\n\t\t\t\thandleItemAsRootObject = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// How do we handle this JSON item from the OneDrive API?\n\t\t// Is this a confirmed 'root' item, has no Parent ID, or is a Deleted Item\n\t\tif (handleItemAsRootObject || !itemHasParentReferenceId || itemIsDeletedOnline){\n\t\t\t// Is a root item, has no id in parentReference or is a OneDrive deleted item\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"objectParentDriveId                                  = \" ~ objectParentDriveId, [\"debug\"]);\n\t\t\t\taddLogEntry(\"handleItemAsRootObject                               = \" ~ to!string(handleItemAsRootObject), [\"debug\"]);\n\t\t\t\taddLogEntry(\"itemHasParentReferenceId                             = \" ~ to!string(itemHasParentReferenceId), [\"debug\"]);\n\t\t\t\taddLogEntry(\"itemIsDeletedOnline                                  = \" ~ to!string(itemIsDeletedOnline), [\"debug\"]);\n\t\t\t\taddLogEntry(\"Handling change immediately as 'root item', or has no parent reference id or is a deleted item\", [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// OK ... do something with this JSON post here ....\n\t\t\tprocessRootAndDeletedJSONItems(onedriveJSONItem, objectParentDriveId, handleItemAsRootObject, itemIsDeletedOnline, itemHasParentReferenceId);\n\t\t} else {\n\t\t\t// Do we need to update this RAW JSON from OneDrive?\n\t\t\t\n\t\t\tbool sharedFolderRenameCheck = false;\n\t\t\t\n\t\t\t// What account type is this?\n\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t// flag this by default as we always sync personal shared folders by default\n\t\t\t\tsharedFolderRenameCheck = true;\n\t\t\t} else {\n\t\t\t\t// business | DocumentLibrary\n\t\t\t\tif (appConfig.getValueBool(\"sync_business_shared_items\")) {\n\t\t\t\t\t// flag this\n\t\t\t\t\tsharedFolderRenameCheck = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\tobjectParentDriveId = transformToLowerCase(objectParentDriveId);\n\t\t\t}\n\t\t\t\n\t\t\t// Do we check if this JSON needs updating?\n\t\t\tif ((objectParentDriveId != appConfig.defaultDriveId) && (sharedFolderRenameCheck)) {\n\t\t\t\t// Potentially need to update this JSON data\n\t\t\t\tif (debugLogging) {addLogEntry(\"Potentially need to update this source JSON .... need to check the database\", [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Check the DB for 'remote' objects, searching 'remoteDriveId' and 'remoteId' items for this remoteItem.driveId and remoteItem.id\n\t\t\t\tItem remoteDBItem;\n\t\t\t\titemDB.selectByRemoteId(objectParentDriveId, thisItemId, remoteDBItem);\n\t\t\t\t\n\t\t\t\t// Is the data that was returned from the database what we are looking for?\n\t\t\t\tif ((remoteDBItem.remoteDriveId == objectParentDriveId) && (remoteDBItem.remoteId == thisItemId)) {\n\t\t\t\t\t// Yes, this is the record we are looking for\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"DB Item response for remoteDBItem: \" ~ to!string(remoteDBItem), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t\t// Must compare remoteDBItem.name with remoteItem.name\n\t\t\t\t\tif (remoteDBItem.name != onedriveJSONItem[\"name\"].str) {\n\t\t\t\t\t\t// Update JSON Item\n\t\t\t\t\t\tstring actualOnlineName = onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"Updating source JSON 'name' to that which is the actual local directory\", [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"onedriveJSONItem['name'] was:         \" ~ onedriveJSONItem[\"name\"].str, [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"Updating onedriveJSONItem['name'] to: \" ~ remoteDBItem.name, [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tonedriveJSONItem[\"name\"] = remoteDBItem.name;\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"onedriveJSONItem['name'] now:         \" ~ onedriveJSONItem[\"name\"].str, [\"debug\"]);}\n\t\t\t\t\t\t// Add the original name to the JSON\n\t\t\t\t\t\tonedriveJSONItem[\"actualOnlineName\"] = actualOnlineName;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Do we discard this JSON item?\n\t\t\tbool discardDeltaJSONItem = false;\n\t\t\t\n\t\t\t// Microsoft OneNote container objects present neither folder or file but contain a 'package' element\n\t\t\t// \"package\": {\n\t\t\t//\t\t\t\"type\": \"oneNote\"\n\t\t\t//\t\t},\n\t\t\t// Confirmed with Microsoft OneDrive Personal\n\t\t\t// Confirmed with Microsoft OneDrive Business\n\t\t\tif (isOneNotePackageFolder(onedriveJSONItem)) {\n\t\t\t\t// This JSON has this element\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - The Microsoft OneNote Notebook Package '\" ~ generatePathFromJSONData(onedriveJSONItem) ~ \"' is not supported by this client\", [\"verbose\"]);}\n\t\t\t\tdiscardDeltaJSONItem = true;\n\t\t\t\t\n\t\t\t\t// Add this 'id' to onenotePackageIdentifiers as a future 'catch all' for any objects inside this container\n\t\t\t\tif (!onenotePackageIdentifiers.canFind(thisItemId)) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Adding 'thisItemId' to onenotePackageIdentifiers: \" ~ to!string(thisItemId), [\"debug\"]);}\n\t\t\t\t\tonenotePackageIdentifiers ~= thisItemId;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t// 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\n\t\t\t// Is there a 'file' JSON element and it has a 'mimeType' element?\n\t\t\tif (isItemFile(onedriveJSONItem) && hasMimeType(onedriveJSONItem)) {\n\t\t\t\t// Is the mimeType 'application/msonenote' or 'application/octet-stream'\n\t\t\t\t\n\t\t\t\t// However there is API inconsistency here between Personal and Business Accounts\n\t\t\t\t// Personal OneNote .onetoc2 and .one items all report mimeType as 'application/msonenote'\n\t\t\t\t// Business OneNote .onetoc2 and .one items however are different:\n\t\t\t\t//  .one = 'application/msonenote' mimeType\n\t\t\t\t//  .onetoc2 = 'application/octet-stream' mimeType\n\t\t\t\tif (isMicrosoftOneNoteMimeType1(onedriveJSONItem) || isMicrosoftOneNoteMimeType2(onedriveJSONItem)) {\n\t\t\t\t\t// We have a 'mimeType' match\n\t\t\t\t\t// What is the file extension?\n\t\t\t\t\t// .one (Type1)\n\t\t\t\t\t// .onetoc2 (Type2)\n\t\t\t\t\tif (isMicrosoftOneNoteFileExtensionType1(onedriveJSONItem) || isMicrosoftOneNoteFileExtensionType2(onedriveJSONItem)) {\n\t\t\t\t\t\t// Extreme confidence this JSON is a Microsoft OneNote file reference which cannot be supported\n\t\t\t\t\t\t// Log that this will be skipped as this this is a Microsoft OneNote item and unsupported\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - The Microsoft OneNote Notebook File '\" ~ generatePathFromJSONData(onedriveJSONItem) ~ \"' is not supported by this client\", [\"verbose\"]);}\n\t\t\t\t\t\tdiscardDeltaJSONItem = true;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Add the Parent ID to onenotePackageIdentifiers\n\t\t\t\t\t\tif (itemHasParentReferenceId) {\n\t\t\t\t\t\t\t// Add this 'id' to onenotePackageIdentifiers as a future 'catch all' for any objects inside this container\n\t\t\t\t\t\t\tif (!onenotePackageIdentifiers.canFind(objectParentId)) {\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Adding 'objectParentId' to onenotePackageIdentifiers: \" ~ to!string(objectParentId), [\"debug\"]);}\n\t\t\t\t\t\t\t\tonenotePackageIdentifiers ~= objectParentId;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Microsoft OneDrive OneNote 'internal recycle bin' items are a 'folder' , with a 'size' but have a specific name 'OneNote_RecycleBin', for example:\n\t\t\t//\t{\n\t\t\t//\t\t....\n\t\t\t//\t\t\"fileSystemInfo\": {\n\t\t\t//\t\t\t\"createdDateTime\": \"2025-03-10T17:11:15Z\",\n\t\t\t//\t\t\t\"lastModifiedDateTime\": \"2025-03-10T17:11:15Z\"\n\t\t\t//\t\t},\n\t\t\t//\t\t\"folder\": {\n\t\t\t//\t\t\t\"childCount\": 2\n\t\t\t//\t\t},\n\t\t\t//\t\t\"id\": \"XXXXX\",\n\t\t\t//\t\t\"lastModifiedBy\": {\n\t\t\t//\t\t\tXXXXX\n\t\t\t//\t\t},\n\t\t\t//\t\t\"name\": \"OneNote_RecycleBin\",\n\t\t\t//\t\t\"parentReference\": {\n\t\t\t//\t\t\t\"driveId\": \"abcde\",\n\t\t\t//\t\t\t\"driveType\": \"business\",\n\t\t\t//\t\t\t\"id\": \"abcde\",\n\t\t\t//\t\t\t\"name\": \"PARENT NAME - ONENOTE PACKAGE NAME\",\n\t\t\t//\t\t\t\"path\": \"/drives/path/to/parent\",\n\t\t\t//\t\t\t\"siteId\": \"XXXXX\"\n\t\t\t//\t\t},\n\t\t\t//\t\t\"size\": 17468\n\t\t\t//\t}\n\t\t\t// \n\t\t\t// The only way we can block this download is looking at the 'name' component\n\t\t\tif (onedriveJSONItem[\"name\"].str == \"OneNote_RecycleBin\") {\n\t\t\t\t// Log that this will be skipped as this this is a Microsoft OneNote item and unsupported\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - The Microsoft OneNote Notebook Recycle Bin '\" ~ generatePathFromJSONData(onedriveJSONItem) ~ \"' is not supported by this client\", [\"verbose\"]);}\n\t\t\t\tdiscardDeltaJSONItem = true;\n\t\t\t\t\n\t\t\t\t// Add the Parent ID to onenotePackageIdentifiers\n\t\t\t\tif (itemHasParentReferenceId) {\n\t\t\t\t\t// Add this 'id' to onenotePackageIdentifiers as a future 'catch all' for any objects inside this container\n\t\t\t\t\tif (!onenotePackageIdentifiers.canFind(objectParentId)) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Adding 'objectParentId' to onenotePackageIdentifiers: \" ~ to!string(objectParentId), [\"debug\"]);}\n\t\t\t\t\t\tonenotePackageIdentifiers ~= objectParentId;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// If we are not self-generating a /delta response, check this initial /delta JSON bundle item against the basic checks \n\t\t\t// of applicability against 'skip_file', 'skip_dir' and 'sync_list'\n\t\t\t// We only do this if we did not generate a /delta response, as generateDeltaResponse() performs the checkJSONAgainstClientSideFiltering()\n\t\t\t// against elements as it is building the /delta compatible response\n\t\t\t// If we blindly just 'check again' all JSON responses then there is potentially double JSON processing going on if we used generateDeltaResponse()\n\t\t\tif (!generateSimulatedDeltaResponse) {\n\t\t\t\t// Did we already exclude?\n\t\t\t\tif (!discardDeltaJSONItem) {\n\t\t\t\t\t// Check applicability against 'skip_file', 'skip_dir' and 'sync_list'\n\t\t\t\t\tdiscardDeltaJSONItem = checkJSONAgainstClientSideFiltering(onedriveJSONItem);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Add this JSON item for further processing if this is not being discarded\n\t\t\tif (!discardDeltaJSONItem) {\n\t\t\t\t// If 'personal' account type, we must validate [\"parentReference\"][\"driveId\"] value in this raw JSON\n\t\t\t\t// Issue #3115 - Validate driveId length\n\t\t\t\t// What account type is this?\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\n\t\t\t\t\tstring existingDriveIdEntry = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tstring newDriveIdEntry;\n\t\t\t\t\t\n\t\t\t\t\t// Perform the required length test\n\t\t\t\t\tif (existingDriveIdEntry.length < 16) {\n\t\t\t\t\t\t// existingDriveIdEntry value is not 16 characters in length\n\t\t\t\t\t\n\t\t\t\t\t\t// Is this 'driveId' in this JSON a 15 character representation of our actual 'driveId' which we have already corrected?\n\t\t\t\t\t\tif (appConfig.defaultDriveId.canFind(existingDriveIdEntry)) {\n\t\t\t\t\t\t\t// The JSON provided value is our 'driveId'\n\t\t\t\t\t\t\t// Debug logging for correction\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"ONEDRIVE PERSONAL API BUG (Issue #3072): The provided raw JSON ['parentReference']['driveId'] value is not 16 Characters in length - correcting with validated 'appConfig.defaultDriveId' value\", [\"debug\"]);}\n\t\t\t\t\t\t\tnewDriveIdEntry = appConfig.defaultDriveId;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// No match, potentially a Shared Folder ... \n\t\t\t\t\t\t\t// Debug logging for correction\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"ONEDRIVE PERSONAL API BUG (Issue #3072): The provided raw JSON ['parentReference']['driveId'] value is not 16 Characters in length - padding with leading zero's\", [\"debug\"]);}\n\t\t\t\t\t\t\t// Generate the change\n\t\t\t\t\t\t\tnewDriveIdEntry = to!string(existingDriveIdEntry.padLeft('0', 16)); // Explicitly use padLeft for leading zero padding, leave case as-is\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Make the change to the JSON data before submit for further processing\n\t\t\t\t\t\tonedriveJSONItem[\"parentReference\"][\"driveId\"] = newDriveIdEntry;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\n\t\t\t\t// Add onedriveJSONItem to jsonItemsToProcess\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"Adding this raw JSON OneDrive Item to jsonItemsToProcess array for further processing\", [\"debug\"]);\n\t\t\t\t\tif (itemIsRemoteItem) {\n\t\t\t\t\t\taddLogEntry(\"- This JSON record represents a online remote folder, thus needs special handling when being processed further\", [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjsonItemsToProcess ~= onedriveJSONItem;\n\t\t\t} else {\n\t\t\t\t// detail we are discarding the json\n\t\t\t\tif (debugLogging) {addLogEntry(\"Discarding this raw JSON OneDrive Item as this has been determined to be unwanted\", [\"debug\"]);}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// How long to initially process this JSON item\n\t\tif (debugLogging) {\n\t\t\tDuration jsonProcessingElapsedTime = MonoTime.currTime() - jsonProcessingStartTime;\n\t\t\taddLogEntry(\"Initial JSON item processing time: \" ~ to!string(jsonProcessingElapsedTime), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Process 'root' and 'deleted' OneDrive JSON items\n\tvoid processRootAndDeletedJSONItems(JSONValue onedriveJSONItem, string driveId, bool handleItemAsRootObject, bool itemIsDeletedOnline, bool itemHasParentReferenceId) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Use the JSON elements rather can computing a DB struct via makeItem()\n\t\tstring thisItemId = onedriveJSONItem[\"id\"].str;\n\t\tstring thisItemDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\n\t\t// Check if the item has been seen before\n\t\tItem existingDatabaseItem;\n\t\tbool existingDBEntry = itemDB.selectById(thisItemDriveId, thisItemId, existingDatabaseItem);\n\t\t\n\t\t// Is the item deleted online?\n\t\tif(!itemIsDeletedOnline) {\n\t\t\t// Is the item a confirmed root object?\n\t\t\t\n\t\t\t// The JSON item should be considered a 'root' item if:\n\t\t\t// 1. Contains a [\"root\"] element\n\t\t\t// 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\n\t\t\t// 2. Has no [\"parentReference\"][\"path\"]\n\t\t\t// 3. Was detected by an input flag as to be handled as a root item regardless of actual status\n\t\t\t\n\t\t\tif ((handleItemAsRootObject) || (!itemHasParentReferenceId)) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Handing JSON object as OneDrive 'root' object\", [\"debug\"]);}\n\t\t\t\tif (!existingDBEntry) {\n\t\t\t\t\t// we have not seen this item before\n\t\t\t\t\tsaveItem(onedriveJSONItem);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Change is to delete an item\n\t\t\tif (debugLogging) {addLogEntry(\"Handing a OneDrive Online Deleted Item\", [\"debug\"]);}\n\t\t\t\n\t\t\t// Is the deleted item in our database?\n\t\t\tif (existingDBEntry) {\n\t\t\t\t// Is the item to delete locally actually in sync with OneDrive currently?\n\t\t\t\t// What is the source of this item data?\n\t\t\t\tstring itemSource = \"online\";\n\t\t\t\t\n\t\t\t\t// Compute this deleted items path based on the database entries\n\t\t\t\tstring localPathToDelete = computeItemPath(existingDatabaseItem.driveId, existingDatabaseItem.parentId) ~ \"/\" ~ existingDatabaseItem.name;\n\t\t\t\tif (isItemSynced(existingDatabaseItem, localPathToDelete, itemSource)) {\n\t\t\t\t\t// Flag to delete\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Flagging to delete item locally due to online deletion event: \" ~ to!string(onedriveJSONItem), [\"debug\"]);}\n\t\t\t\t\t// Use the DB entries returned - add the driveId, itemId and parentId values  to the array\n\t\t\t\t\tidsToDelete ~= [existingDatabaseItem.driveId, existingDatabaseItem.id, existingDatabaseItem.parentId];\n\t\t\t\t} else {\n\t\t\t\t\t// Local item is not in sync with the online item, but the online item has been deleted, and we are flagging to delete the local item\n\t\t\t\t\t// We need to determine the trigger for isItemSynced() returning false before we determine if we should make utilise safeBackup()\n\t\t\t\t\t// Is this the exact same file?\n\t\t\t\t\t// Test the file hash against the hash of the file online\n\t\t\t\t\t\n\t\t\t\t\t// Empirical evidence shows that Microsoft do not provide a 'valid' hash in JSON data for online deleted items, for example:\n\t\t\t\t\t//   file\":{\"hashes\":{\"quickXorHash\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}},\n\t\t\t\t\t// Thus this makes using the provided data via the API useless for a hash comparison test\n\t\t\t\t\t\n\t\t\t\t\t// Test the existing database item hash against the hash on the local disk - as this is what we know was in-sync with online prior to online deletion event\n\t\t\t\t\tif (!testFileHash(localPathToDelete, existingDatabaseItem)) {\n\t\t\t\t\t\t// Current file on disk is different by hash / content\n\t\t\t\t\t\t// If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not\n\t\t\t\t\t\t// In case the renamed path is needed\n\t\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t\tsafeBackup(localPathToDelete, dryRun, bypassDataPreservation, renamedPath);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Purge the old record from the database as this still exists. The safeBackup() generated file now will be 'new' on the local filesystem\n\t\t\t\t\t\titemDB.deleteById(existingDatabaseItem.driveId, existingDatabaseItem.id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Hash is the same, we can assume the isItemSynced() returning false was due to some sort of timestamp issue\n\t\t\t\t\t\t// Flag to delete rather than create a backup of the local file\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Flagging to delete item locally due to online deletion event: \" ~ to!string(onedriveJSONItem), [\"debug\"]);}\n\t\t\t\t\t\t// Use the DB entries returned - add the driveId, itemId and parentId values  to the array\n\t\t\t\t\t\tidsToDelete ~= [existingDatabaseItem.driveId, existingDatabaseItem.id, existingDatabaseItem.parentId];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Flag to ignore\n\t\t\t\tif (debugLogging) {addLogEntry(\"Flagging item to skip: \" ~ to!string(onedriveJSONItem), [\"debug\"]);}\n\t\t\t\tskippedItems.insert(thisItemId);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Process each of the elements contained in jsonItemsToProcess[]\n\tvoid processJSONItemsInBatch(JSONValue[] array, long batchGroup, long batchCount) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tlong batchElementCount = array.length;\n\t\tMonoTime jsonProcessingStartTime;\n\n\t\tforeach (i, onedriveJSONItem; array.enumerate) {\n\t\t\t// Use the JSON elements rather can computing a DB struct via makeItem()\n\t\t\tlong elementCount = i +1;\n\t\t\tjsonProcessingStartTime = MonoTime.currTime();\n\t\t\t\n\t\t\t// To show this is the processing for this particular item, start off with this breaker line\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(debugLogBreakType1, [\"debug\"]);\n\t\t\t\taddLogEntry(\"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\"]);\n\t\t\t\taddLogEntry(\"Raw JSON OneDrive Item (Batched Item): \" ~ to!string(onedriveJSONItem), [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Configure required items from the JSON elements\n\t\t\tstring thisItemId = onedriveJSONItem[\"id\"].str;\n\t\t\tstring thisItemDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\tstring thisItemParentId = onedriveJSONItem[\"parentReference\"][\"id\"].str;\n\t\t\tstring thisItemName = onedriveJSONItem[\"name\"].str;\n\t\t\t\n\t\t\t// Create an empty item struct for an existing DB item\n\t\t\tItem existingDatabaseItem;\n\t\t\t\n\t\t\t// Do we NOT want this item?\n\t\t\tbool unwanted = false; // meaning by default we will WANT this item\n\t\t\t// Is this parent is in the database\n\t\t\tbool parentInDatabase = false;\n\t\t\t// Is this the 'root' folder of a Shared Folder\n\t\t\tbool rootSharedFolder = false;\n\t\t\t\n\t\t\t// What is the full path of the new item\n\t\t\tstring computedItemPath;\n\t\t\tstring newItemPath;\n\t\t\t\n\t\t\t// Configure the remoteItem - so if it is used, it can be utilised later\n\t\t\tItem remoteItem;\n\t\t\t\n\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\tthisItemDriveId = transformToLowerCase(thisItemDriveId);\n\t\t\t}\n\t\t\t\n\t\t\t// Check the database for an existing entry for this JSON item\n\t\t\tbool existingDBEntry = itemDB.selectById(thisItemDriveId, thisItemId, existingDatabaseItem);\n\t\t\t\n\t\t\t// Calculate if the Parent Item is in the database so that it can be re-used\n\t\t\tparentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId);\n\t\t\t\n\t\t\t// Calculate the local path of this JSON item, but we can only do this if the parent is in the database\n\t\t\tif (parentInDatabase) {\n\t\t\t\t// Compute the full local path for an item based on its position within the OneDrive hierarchy\n\t\t\t\t// This also accounts for Shared Folders in our account root, plus Shared Folders in a folder (relocated shared folders)\n\t\t\t\tcomputedItemPath = computeItemPath(thisItemDriveId, thisItemParentId);\n\t\t\t\t\n\t\t\t\t// Is 'thisItemParentId' in the DB as a 'root' object?\n\t\t\t\tItem databaseItem;\n\t\t\t\t\n\t\t\t\t// Is this a remote drive?\n\t\t\t\tif (thisItemDriveId != appConfig.defaultDriveId) {\n\t\t\t\t\t// query the database for the actual thisItemParentId record\n\t\t\t\t\titemDB.selectById(thisItemDriveId, thisItemParentId, databaseItem);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Calculate newItemPath to\n\t\t\t\t// This needs to factor in:\n\t\t\t\t// - Shared Folders = ItemType.root with a name of 'root'\n\t\t\t\t// - SharePoint Document Root = ItemType.root with a name of the actual shared folder\n\t\t\t\t// - Relocatable Shared Folders where a user moves a Shared Folder Link to a sub folder elsewhere within their directory structure online\n\t\t\t\tif (databaseItem.type == ItemType.root) {\n\t\t\t\t\t// 'root' database object\n\t\t\t\t\tif (databaseItem.name == \"root\") {\n\t\t\t\t\t\t// OneDrive Business Shared Folder 'root' shortcut link\n\t\t\t\t\t\t// If the record type is now a root record, we dont want to add the name to itself\n\t\t\t\t\t\tnewItemPath = computedItemPath;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// OneDrive Business SharePoint Document 'root' shortcut link\n\t\t\t\t\t\tif (databaseItem.name == thisItemName) {\n\t\t\t\t\t\t\t// If the record type is now a root record, we dont want to add the name to itself\n\t\t\t\t\t\t\tnewItemPath = computedItemPath;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// add the item name to the computed path\n\t\t\t\t\t\t\tnewItemPath = computedItemPath ~ \"/\" ~ thisItemName;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Set this for later use\n\t\t\t\t\trootSharedFolder = true;\n\t\t\t\t} else {\n\t\t\t\t\t// Add the item name to the computed path\n\t\t\t\t\tnewItemPath = computedItemPath ~ \"/\" ~ thisItemName;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// debug logging of what was calculated\n\t\t\t\tif (debugLogging) {addLogEntry(\"JSON Item calculated full path is: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t} else {\n\t\t\t\t// Parent not in the database\n\t\t\t\t// Is the parent a 'folder' from another user? ie - is this a 'shared folder' that has been shared with us?\n\t\t\t\t\n\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\tthisItemDriveId = transformToLowerCase(thisItemDriveId);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Lets determine why?\n\t\t\t\tif (thisItemDriveId == appConfig.defaultDriveId) {\n\t\t\t\t\t// Parent path does not exist - flagging as unwanted\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Flagging as unwanted: thisItemDriveId (\" ~ thisItemDriveId ~ \"), thisItemParentId (\" ~ thisItemParentId ~ \") not in local database\", [\"debug\"]);}\n\t\t\t\t\t// Was this a skipped item?\n\t\t\t\t\tif (thisItemParentId in skippedItems) {\n\t\t\t\t\t\t// Parent is a skipped item\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Reason: thisItemParentId listed within skippedItems\", [\"debug\"]);}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Parent is not in the database, as we are not creating it\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Reason: Parent ID is not in the DB .. \", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Flag as unwanted\n\t\t\t\t\tunwanted = true;\t\n\t\t\t\t} else {\n\t\t\t\t\t// Format the OneDrive change into a consumable object for the database\n\t\t\t\t\tremoteItem = makeItem(onedriveJSONItem);\n\t\t\t\t\t\n\t\t\t\t\t// Edge case as the parent (from another users OneDrive account) will never be in the database - potentially a shared object?\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"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\"]);\n\t\t\t\t\t\taddLogEntry(\"Potential Shared Object JSON: \" ~ sanitiseJSONItem(onedriveJSONItem), [\"debug\"]);\n\t\t\t\t\t}\n\n\t\t\t\t\t// What account type is this?\t\t\t\t\t\n\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t// Personal Account Handling\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Handling a Personal Shared Item JSON object\", [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Does the JSON have a shared element structure\n\t\t\t\t\t\tif (hasSharedElement(onedriveJSONItem)) {\n\t\t\t\t\t\t\t// Has the Shared JSON structure\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Personal Shared Item JSON object has the 'shared' JSON structure\", [\"debug\"]);}\n\t\t\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(onedriveJSONItem);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// The Shared JSON structure is missing .....\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Personal Shared Item JSON object is MISSING the 'shared' JSON structure ... API BUG ?\", [\"debug\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Ensure that this item has no parent\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting remoteItem.parentId of Personal Shared Item JSON object to be null\", [\"debug\"]);}\n\t\t\t\t\t\tremoteItem.parentId = null;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Add this record to the local database\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Update/Insert local database with Personal Shared Item JSON object with remoteItem.parentId as null: \" ~ to!string(remoteItem), [\"debug\"]);}\n\t\t\t\t\t\titemDB.upsert(remoteItem);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Due to OneDrive API inconsistency with Personal Accounts, again with European Data Centres, as we have handled this JSON - flag as unwanted as processing is complete for this JSON item\n\t\t\t\t\t\tunwanted = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Business or SharePoint Account Handling\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Handling a Business or SharePoint Shared Item JSON object\", [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (appConfig.accountType == \"business\") {\n\t\t\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(onedriveJSONItem);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Ensure that this item has no parent\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting remoteItem.parentId to be null\", [\"debug\"]);}\n\t\t\t\t\t\t\tremoteItem.parentId = null;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Check the DB for 'remote' objects, searching 'remoteDriveId' and 'remoteId' items for this remoteItem.driveId and remoteItem.id\n\t\t\t\t\t\t\tItem remoteDBItem;\n\t\t\t\t\t\t\titemDB.selectByRemoteId(remoteItem.driveId, remoteItem.id, remoteDBItem);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Must compare remoteDBItem.name with remoteItem.name\n\t\t\t\t\t\t\tif ((!remoteDBItem.name.empty) && (remoteDBItem.name != remoteItem.name)) {\n\t\t\t\t\t\t\t\t// Update DB Item\n\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"The shared item stored in OneDrive, has a different name to the actual name on the remote drive\", [\"debug\"]);\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Updating remoteItem.name JSON data with the actual name being used on account drive and local folder\", [\"debug\"]);\n\t\t\t\t\t\t\t\t\taddLogEntry(\"remoteItem.name was:              \" ~ remoteItem.name, [\"debug\"]);\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Updating remoteItem.name to:      \" ~ remoteDBItem.name, [\"debug\"]);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tremoteItem.name = remoteDBItem.name;\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting remoteItem.remoteName to: \" ~ onedriveJSONItem[\"name\"].str, [\"debug\"]);}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Update JSON Item\n\t\t\t\t\t\t\t\tremoteItem.remoteName = onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Updating source JSON 'name' to that which is the actual local directory\", [\"debug\"]);\n\t\t\t\t\t\t\t\t\taddLogEntry(\"onedriveJSONItem['name'] was:         \" ~ onedriveJSONItem[\"name\"].str, [\"debug\"]);\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Updating onedriveJSONItem['name'] to: \" ~ remoteDBItem.name, [\"debug\"]);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tonedriveJSONItem[\"name\"] = remoteDBItem.name;\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"onedriveJSONItem['name'] now:         \" ~ onedriveJSONItem[\"name\"].str, [\"debug\"]);}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Update newItemPath value\n\t\t\t\t\t\t\t\tnewItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ \"/\" ~ remoteDBItem.name;\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"New Item updated calculated full path is: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Add this record to the local database\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Update/Insert local database with remoteItem details: \" ~ to!string(remoteItem), [\"debug\"]);}\n\t\t\t\t\t\t\titemDB.upsert(remoteItem);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Sharepoint account type\n\t\t\t\t\t\t\taddLogEntry(\"Handling a SharePoint Shared Item JSON object - NOT IMPLEMENTED YET ........ RAISE A BUG PLEASE\", [\"info\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check the skippedItems array for the parent id of this JSONItem if this is something we need to skip\n\t\t\tif (!unwanted) {\n\t\t\t\tif (thisItemParentId in skippedItems) {\n\t\t\t\t\t// Flag this JSON item as unwanted\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Flagging as unwanted: find(thisItemParentId).length != 0\", [\"debug\"]);}\n\t\t\t\t\tunwanted = true;\n\t\t\t\t\t\n\t\t\t\t\t// Is this item id in the database?\n\t\t\t\t\tif (existingDBEntry) {\n\t\t\t\t\t\t// item exists in database, most likely moved out of scope for current client configuration\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"This item was previously synced / seen by the client\", [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\tif ((\"name\" in onedriveJSONItem[\"parentReference\"]) != null) {\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// How is this item now out of scope?\n\t\t\t\t\t\t\t// is sync_list configured\n\t\t\t\t\t\t\tif (syncListConfigured) {\n\t\t\t\t\t\t\t\t// sync_list configured and in use\n\t\t\t\t\t\t\t\tif (selectiveSync.isPathExcludedViaSyncList(onedriveJSONItem[\"parentReference\"][\"name\"].str)) {\n\t\t\t\t\t\t\t\t\t// Previously synced item is now out of scope as it has been moved out of what is included in sync_list\n\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"This previously synced item is now excluded from being synced due to sync_list exclusion\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// flag to delete local file as it now is no longer in sync with OneDrive\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Flagging to delete item locally as this is now an unwanted item (parental exclusion) and the item currently exists in the local database: \", [\"verbose\"]);}\n\t\t\t\t\t\t\t// Use the configured values - add the driveId, itemId and parentId values to the array\n\t\t\t\t\t\t\tidsToDelete ~= [thisItemDriveId, thisItemId, thisItemParentId];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\t\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check the item type - if it not an item type that we support, we cant process the JSON item\n\t\t\tif (!unwanted) {\n\t\t\t\tif (isItemFile(onedriveJSONItem)) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"The JSON item we are processing is a file\", [\"debug\"]);}\n\t\t\t\t} else if (isItemFolder(onedriveJSONItem)) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"The JSON item we are processing is a folder\", [\"debug\"]);}\n\t\t\t\t} else if (isItemRemote(onedriveJSONItem)) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"The JSON item we are processing is a remote item\", [\"debug\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// Why was this unwanted?\n\t\t\t\t\tif (newItemPath.empty) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"OOPS: newItemPath is empty ....... need to calculate it\", [\"debug\"]);}\n\t\t\t\t\t\t// Compute this item path & need the full path for this file\n\t\t\t\t\t\tnewItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ \"/\" ~ thisItemName;\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"New Item calculated full path is: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t// Microsoft OneNote container objects present as neither folder or file but has file size\n\t\t\t\t\tif ((!isItemFile(onedriveJSONItem)) && (!isItemFolder(onedriveJSONItem)) && (hasFileSize(onedriveJSONItem))) {\n\t\t\t\t\t\t// Log that this was skipped as this was a Microsoft OneNote item and unsupported\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The Microsoft OneNote Notebook '\" ~ newItemPath ~ \"' is not supported by this client\", [\"verbose\"]);}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Log that this item was skipped as unsupported \n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The OneDrive item '\" ~ newItemPath ~ \"' is not supported by this client\", [\"verbose\"]);}\n\t\t\t\t\t}\n\t\t\t\t\tunwanted = true;\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Flagging as unwanted: item type is not supported\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check if this is excluded by config option: skip_dir\n\t\t\tif (!unwanted) {\n\t\t\t\t// Only check path if config is != \"\"\n\t\t\t\tif (!appConfig.getValueString(\"skip_dir\").empty) {\n\t\t\t\t\t// 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)\n\t\t\t\t\tif ((isItemFolder(onedriveJSONItem)) || (isRemoteFolderItem(onedriveJSONItem))) {\n\t\t\t\t\t\t// work out the 'snippet' path where this folder would be created\n\t\t\t\t\t\tstring simplePathToCheck = \"\";\n\t\t\t\t\t\tstring complexPathToCheck = \"\";\n\t\t\t\t\t\tstring matchDisplay = \"\";\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (hasParentReference(onedriveJSONItem)) {\n\t\t\t\t\t\t\t// we need to workout the FULL path for this item\n\t\t\t\t\t\t\t// simple path calculation\n\t\t\t\t\t\t\tif ((\"name\" in onedriveJSONItem[\"parentReference\"]) != null) {\n\t\t\t\t\t\t\t\t// how do we build the simplePathToCheck path up ?\n\t\t\t\t\t\t\t\t// did we flag this as the root shared folder object earlier?\n\t\t\t\t\t\t\t\tif (rootSharedFolder) {\n\t\t\t\t\t\t\t\t\t// just use item name\n\t\t\t\t\t\t\t\t\tsimplePathToCheck = onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// add parent name to item name\n\t\t\t\t\t\t\t\t\tsimplePathToCheck = onedriveJSONItem[\"parentReference\"][\"name\"].str ~ \"/\" ~ onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// just use item name\n\t\t\t\t\t\t\t\tsimplePathToCheck = onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir path to check (simple):  \" ~ simplePathToCheck, [\"debug\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// complex path calculation\n\t\t\t\t\t\t\tif (parentInDatabase) {\n\t\t\t\t\t\t\t\t// build up complexPathToCheck\n\t\t\t\t\t\t\t\tcomplexPathToCheck = buildNormalizedPath(newItemPath);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Parent details not in database - unable to compute complex path to check\", [\"debug\"]);}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!complexPathToCheck.empty) {\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir path to check (complex): \" ~ complexPathToCheck, [\"debug\"]);}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsimplePathToCheck = onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// If 'simplePathToCheck' or 'complexPathToCheck' is of the following format:  root:/folder\n\t\t\t\t\t\t// then isDirNameExcluded matching will not work\n\t\t\t\t\t\tif (simplePathToCheck.canFind(\":\")) {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updating simplePathToCheck to remove 'root:'\", [\"debug\"]);}\n\t\t\t\t\t\t\tsimplePathToCheck = processPathToRemoveRootReference(simplePathToCheck);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (complexPathToCheck.canFind(\":\")) {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updating complexPathToCheck to remove 'root:'\", [\"debug\"]);}\n\t\t\t\t\t\t\tcomplexPathToCheck = processPathToRemoveRootReference(complexPathToCheck);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// OK .. what checks are we doing?\n\t\t\t\t\t\tif ((!simplePathToCheck.empty) && (complexPathToCheck.empty)) {\n\t\t\t\t\t\t\t// just a simple check\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Performing a simple check only\", [\"debug\"]);}\n\t\t\t\t\t\t\tunwanted = selectiveSync.isDirNameExcluded(simplePathToCheck);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// simple and complex\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Performing a simple then complex path match if required\", [\"debug\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// simple first\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Performing a simple check first\", [\"debug\"]);}\n\t\t\t\t\t\t\tunwanted = selectiveSync.isDirNameExcluded(simplePathToCheck);\n\t\t\t\t\t\t\tmatchDisplay = simplePathToCheck;\n\t\t\t\t\t\t\tif (!unwanted) {\n\t\t\t\t\t\t\t\t// simple didnt match, perform a complex check\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Simple match was false, attempting complex match\", [\"debug\"]);}\n\t\t\t\t\t\t\t\tunwanted = selectiveSync.isDirNameExcluded(complexPathToCheck);\n\t\t\t\t\t\t\t\tmatchDisplay = complexPathToCheck;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// result\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir exclude result (directory based): \" ~ to!string(unwanted), [\"debug\"]);}\n\t\t\t\t\t\tif (unwanted) {\n\t\t\t\t\t\t\t// This path should be skipped\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by skip_dir config: \" ~ matchDisplay, [\"verbose\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Is the item a file?\n\t\t\t\t\t// We need to check to see if this files path is excluded as well\n\t\t\t\t\tif (isItemFile(onedriveJSONItem)) {\n\t\t\t\t\t\n\t\t\t\t\t\tstring pathToCheck;\n\t\t\t\t\t\t// does the newItemPath start with '/'?\n\t\t\t\t\t\tif (!startsWith(newItemPath, \"/\")){\n\t\t\t\t\t\t\t// path does not start with '/', but we need to check skip_dir entries with and without '/'\n\t\t\t\t\t\t\t// so always make sure we are checking a path with '/'\n\t\t\t\t\t\t\tpathToCheck = '/' ~ dirName(newItemPath);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tpathToCheck = dirName(newItemPath);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// perform the check\n\t\t\t\t\t\tunwanted = selectiveSync.isDirNameExcluded(pathToCheck);\n\t\t\t\t\t\t// result\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir exclude result (file based): \" ~ to!string(unwanted), [\"debug\"]);}\n\t\t\t\t\t\tif (unwanted) {\n\t\t\t\t\t\t\t// this files path should be skipped\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - file path is excluded by skip_dir config: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check if this is excluded by config option: skip_file\n\t\t\tif (!unwanted) {\n\t\t\t\t// Is the JSON item a file?\n\t\t\t\tif (isItemFile(onedriveJSONItem)) {\n\t\t\t\t\t// skip_file can contain 4 types of entries:\n\t\t\t\t\t// - wildcard - *.txt\n\t\t\t\t\t// - text + wildcard - name*.txt\n\t\t\t\t\t// - full path + combination of any above two - /path/name*.txt\n\t\t\t\t\t// - full path to file - /path/to/file.txt\n\t\t\t\t\t\n\t\t\t\t\t// is the parent id in the database?\n\t\t\t\t\tif (parentInDatabase) {\n\t\t\t\t\t\t// Compute this item path & need the full path for this file\n\t\t\t\t\t\tif (newItemPath.empty) {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"OOPS: newItemPath is empty ....... need to calculate it\", [\"debug\"]);}\n\t\t\t\t\t\t\tnewItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ \"/\" ~ thisItemName;\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"New Item calculated full path is: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// The path that needs to be checked needs to include the '/'\n\t\t\t\t\t\t// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched\n\t\t\t\t\t\t// However, as 'path' used throughout, use a temp variable with this modification so that we use the temp variable for exclusion checks\n\t\t\t\t\t\tstring exclusionTestPath = \"\";\n\t\t\t\t\t\tif (!startsWith(newItemPath, \"/\")){\n\t\t\t\t\t\t\t// Add '/' to the path\n\t\t\t\t\t\t\texclusionTestPath = '/' ~ newItemPath;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_file item to check: \" ~ exclusionTestPath, [\"debug\"]);}\n\t\t\t\t\t\tunwanted = selectiveSync.isFileNameExcluded(exclusionTestPath);\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Result: \" ~ to!string(unwanted), [\"debug\"]);}\n\t\t\t\t\t\tif (unwanted) {\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - excluded by skip_file config: \" ~ thisItemName, [\"verbose\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// parent id is not in the database\n\t\t\t\t\t\tunwanted = true;\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - parent path not present in local database\", [\"verbose\"]);}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check if this is included or excluded by use of sync_list\n\t\t\tif (!unwanted) {\n\t\t\t\t// No need to try and process something against a sync_list if it has been configured\n\t\t\t\tif (syncListConfigured) {\n\t\t\t\t\t// Compute the item path if empty - as to check sync_list we need an actual path to check\n\t\t\t\t\tif (newItemPath.empty) {\n\t\t\t\t\t\t// Calculate this items path\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"OOPS: newItemPath is empty ....... need to calculate it\", [\"debug\"]);}\n\t\t\t\t\t\tnewItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ \"/\" ~ thisItemName;\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"New Item calculated full path is: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// What path are we checking?\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Path to check against 'sync_list' entries: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Unfortunately there is no avoiding this call to check if the path is excluded|included via sync_list\n\t\t\t\t\tif (selectiveSync.isPathExcludedViaSyncList(newItemPath)) {\n\t\t\t\t\t\t// selective sync advised to skip, however is this a file and are we configured to upload / download files in the root?\n\t\t\t\t\t\tif ((isItemFile(onedriveJSONItem)) && (appConfig.getValueBool(\"sync_root_files\")) && (rootName(newItemPath) == \"\") ) {\n\t\t\t\t\t\t\t// This is a file\n\t\t\t\t\t\t\t// We are configured to sync all files in the root\n\t\t\t\t\t\t\t// This is a file in the logical configured root\n\t\t\t\t\t\t\tunwanted = false;\n\t\t\t\t\t\t\t// Log that we are retaining this file and why\n\t\t\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\t\t\taddLogEntry(\"Path retained due to 'sync_root_files' override for logical root file: \" ~ newItemPath, [\"verbose\"]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// path is unwanted - excluded by 'sync_list'\n\t\t\t\t\t\t\tunwanted = true;\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by sync_list config: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t// flagging to skip this item now, but does this exist in the DB thus needs to be removed / deleted?\n\t\t\t\t\t\t\tif (existingDBEntry) {\n\t\t\t\t\t\t\t\t// flag to delete\n\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Flagging to delete item locally as this is now an unwanted item (sync_list exclusion) and the item currently exists in the local database: \", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t// Use the configured values - add the driveId, itemId and parentId values to the array\n\t\t\t\t\t\t\t\tidsToDelete ~= [thisItemDriveId, thisItemId, thisItemParentId];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check if the user has configured to skip downloading .files or .folders: skip_dotfiles\n\t\t\tif (!unwanted) {\n\t\t\t\tif (appConfig.getValueBool(\"skip_dotfiles\")) {\n\t\t\t\t\tif (isDotFile(newItemPath)) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping item - .file or .folder: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\tunwanted = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check if this should be skipped due to a --check-for-nosync directive (.nosync)?\n\t\t\tif (!unwanted) {\n\t\t\t\tif (appConfig.getValueBool(\"check_nosync\")) {\n\t\t\t\t\t// need the parent path for this object\n\t\t\t\t\tstring parentPath = dirName(newItemPath);\n\t\t\t\t\t// Check for the presence of a .nosync in the parent path\n\t\t\t\t\tif (exists(parentPath ~ \"/.nosync\")) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping downloading item - .nosync found in parent folder & --check-for-nosync is enabled: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\tunwanted = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check if this is excluded by a user set maximum filesize to download\n\t\t\tif (!unwanted) {\n\t\t\t\tif (isItemFile(onedriveJSONItem)) {\n\t\t\t\t\tif (fileSizeLimit != 0) {\n\t\t\t\t\t\tif (onedriveJSONItem[\"size\"].integer >= fileSizeLimit) {\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - excluded by skip_size config: \" ~ thisItemName ~ \" (\" ~ to!string(onedriveJSONItem[\"size\"].integer/2^^20) ~ \" MB)\", [\"verbose\"]);}\n\t\t\t\t\t\t\tunwanted = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// At this point all the applicable checks on this JSON object from OneDrive are complete:\n\t\t\t// - skip_file\n\t\t\t// - skip_dir\n\t\t\t// - sync_list\n\t\t\t// - skip_dotfiles\n\t\t\t// - check_nosync\n\t\t\t// - skip_size\n\t\t\t// - We know if this item exists in the DB or not in the DB\n\t\t\t\n\t\t\t// We know if this JSON item is unwanted or not\n\t\t\tif (unwanted) {\n\t\t\t\t// This JSON item is NOT wanted - it is excluded\n\t\t\t\tif (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\"]);}\n\t\t\t\t\n\t\t\t\t// 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\n\t\t\t\tif (!isItemFile(onedriveJSONItem)) {\n\t\t\t\t\tskippedItems.insert(thisItemId);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// This JSON item is wanted - we need to process this JSON item further\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"OneDrive JSON item passed all applicable Client Side Filtering Rules and has been determined this is a wanted item\", [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"Creating newDatabaseItem object using the provided JSON data\", [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Take the JSON item and create a consumable object for eventual database insertion\n\t\t\t\tItem newDatabaseItem = makeItem(onedriveJSONItem);\n\t\t\t\t\n\t\t\t\tif (existingDBEntry) {\n\t\t\t\t\t// The details of this JSON item are already in the DB\n\t\t\t\t\t// Is the item in the DB the same as the JSON data provided - or is the JSON data advising this is an updated file?\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"OneDrive JSON item is an update to an existing local item\", [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Compute the existing item path\n\t\t\t\t\t// NOTE:\n\t\t\t\t\t//\t\tstring existingItemPath = computeItemPath(existingDatabaseItem.driveId, existingDatabaseItem.id);\n\t\t\t\t\t//\n\t\t\t\t\t// This will calculate the path as follows:\n\t\t\t\t\t//\n\t\t\t\t\t//\t\texistingItemPath:     Document.txt\n\t\t\t\t\t//\n\t\t\t\t\t// Whereas above we use the following\n\t\t\t\t\t//\n\t\t\t\t\t//\t\tnewItemPath = computeItemPath(newDatabaseItem.driveId, newDatabaseItem.parentId) ~ \"/\" ~ newDatabaseItem.name;\n\t\t\t\t\t//\n\t\t\t\t\t// Which generates the following path:\n\t\t\t\t\t//\n\t\t\t\t\t//  \tchangedItemPath:      ./Document.txt\n\t\t\t\t\t// \n\t\t\t\t\t// Need to be consistent here with how 'newItemPath' was calculated\n\t\t\t\t\tstring queryDriveID;\n\t\t\t\t\tstring queryParentID;\n\t\t\t\t\t\n\t\t\t\t\t// Must query with a valid driveid entry\n\t\t\t\t\tif (existingDatabaseItem.driveId.empty) {\n\t\t\t\t\t\tqueryDriveID = thisItemDriveId;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tqueryDriveID = existingDatabaseItem.driveId;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Must query with a valid parentid entry\n\t\t\t\t\tif (existingDatabaseItem.parentId.empty) {\n\t\t\t\t\t\tqueryParentID = thisItemParentId;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tqueryParentID = existingDatabaseItem.parentId;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Calculate the existing path\n\t\t\t\t\tstring existingItemPath = computeItemPath(queryDriveID, queryParentID) ~ \"/\" ~ existingDatabaseItem.name;\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"existingItemPath calculated full path is: \" ~ existingItemPath, [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Ensure that this path exists if this is an 'existing' database item\n\t\t\t\t\tif (existingDatabaseItem.type == ItemType.dir) {\n\t\t\t\t\t\tif (!exists(existingItemPath)) {\n\t\t\t\t\t\t\thandleLocalDirectoryCreation(existingDatabaseItem, existingItemPath, onedriveJSONItem);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Attempt to apply this changed item\n\t\t\t\t\tapplyPotentiallyChangedItem(existingDatabaseItem, existingItemPath, newDatabaseItem, newItemPath, onedriveJSONItem);\n\t\t\t\t\t\n\t\t\t\t\t// Is this JSON object a 'remote' item?\n\t\t\t\t\tif(isItemRemote(onedriveJSONItem)) {\n\t\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(onedriveJSONItem);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Action this JSON item as a new item as we have no DB record of it\n\t\t\t\t\t// The actual item may actually exist locally already, meaning that just the database is out-of-date or missing the data due to --resync\n\t\t\t\t\t// But we also cannot compute the newItemPath as the parental objects may not exist as well\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"OneDrive JSON item is potentially a new local item\", [\"debug\"]);}\n\t\t\t\t\t// Attempt to apply this potentially new item\n\t\t\t\t\tapplyPotentiallyNewLocalItem(newDatabaseItem, onedriveJSONItem, newItemPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// How long to process this JSON item in batch\n\t\t\tif (debugLogging) {\n\t\t\t\tDuration jsonProcessingElapsedTime = MonoTime.currTime() - jsonProcessingStartTime;\n\t\t\t\taddLogEntry(\"Batched JSON item processing time: \" ~ to!string(jsonProcessingElapsedTime), [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Tracking as to if this item was processed\n\t\t\tprocessedCount++;\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Perform the download of any required objects in parallel\n\tvoid processDownloadActivities() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\n\t\t// Are there any items to delete locally? Cleanup space locally first\n\t\tif (!idsToDelete.empty) {\n\t\t\t// There are elements that potentially need to be deleted locally\n\t\t\tif (verboseLogging) {addLogEntry(\"Items to potentially delete locally: \" ~ to!string(idsToDelete.length), [\"verbose\"]);}\n\t\t\t\n\t\t\tif (appConfig.getValueBool(\"download_only\")) {\n\t\t\t\t// Download only has been configured\n\t\t\t\tif (cleanupLocalFiles) {\n\t\t\t\t\t// Process online deleted items\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Processing local deletion activity as --download-only & --cleanup-local-files configured\", [\"verbose\"]);}\n\t\t\t\t\tprocessDeleteItems();\n\t\t\t\t} else {\n\t\t\t\t\t// Not cleaning up local files\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping local deletion activity as --download-only has been used\", [\"verbose\"]);}\n\t\t\t\t\t// List files and directories we are not deleting locally\n\t\t\t\t\tlistDeletedItems();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Not using --download-only process normally\n\t\t\t\tprocessDeleteItems();\n\t\t\t}\n\t\t\t// Cleanup array memory\n\t\t\tidsToDelete = [];\n\t\t}\n\t\t\n\t\t// Are there any items to download post fetching and processing the /delta data?\n\t\tif (!fileJSONItemsToDownload.empty) {\n\t\t\t// There are elements to download\n\t\t\taddLogEntry(\"Number of items to download from Microsoft OneDrive: \" ~ to!string(fileJSONItemsToDownload.length));\n\t\t\tdownloadOneDriveItems();\n\t\t\t// Cleanup array memory\n\t\t\tfileJSONItemsToDownload = [];\n\t\t}\n\t\t\n\t\t// Are there any skipped items still?\n\t\tif (!skippedItems.empty) {\n\t\t\t// Cleanup array memory\n\t\t\tskippedItems.clear();\n\t\t}\n\t\t\n\t\t// 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\n\t\tif (!deltaLinkCache.latestDeltaLink.empty) {\n\t\t\tif (debugLogging) {addLogEntry(\"Updating completed deltaLink for driveID \" ~ deltaLinkCache.driveId ~ \" in DB to: \" ~ deltaLinkCache.latestDeltaLink, [\"debug\"]);}\n\t\t\titemDB.setDeltaLink(deltaLinkCache.driveId, deltaLinkCache.itemId, deltaLinkCache.latestDeltaLink);\n\t\t\t\n\t\t\t// 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\n\t\t\tcacheLatestDeltaLink(deltaLinkInfo, deltaLinkCache.driveId, deltaLinkCache.latestDeltaLink);\t\t\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Function to add or update a key pair in the deltaLinkInfo array\n\tvoid cacheLatestDeltaLink(ref DeltaLinkInfo deltaLinkInfo, string driveId, string latestDeltaLink) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tif (driveId !in deltaLinkInfo) {\n\t\t\tif (debugLogging) {addLogEntry(\"Added new latestDeltaLink entry: \" ~ driveId ~ \" -> \" ~ latestDeltaLink, [\"debug\"]);}\n\t\t} else {\n\t\t\tif (debugLogging) {addLogEntry(\"Updated latestDeltaLink entry for \" ~ driveId ~ \" from \" ~ deltaLinkInfo[driveId] ~ \" to \" ~ latestDeltaLink, [\"debug\"]);}\n\t\t}\n\t\tdeltaLinkInfo[driveId] = latestDeltaLink;\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Function to get the latestDeltaLink based on driveId\n\tstring getDeltaLinkFromCache(ref DeltaLinkInfo deltaLinkInfo, string driveId) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tstring cachedDeltaLink;\n\t\tif (driveId in deltaLinkInfo) {\n\t\t\tcachedDeltaLink = deltaLinkInfo[driveId];\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return value\n\t\treturn cachedDeltaLink;\n\t}\n\t\n\t// If the JSON item is not in the database, it is potentially a new item that we need to action\n\tvoid applyPotentiallyNewLocalItem(Item newDatabaseItem, JSONValue onedriveJSONItem, string newItemPath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Due to this function, we need to keep the 'return' code as-is, so that this function operates as efficiently as possible.\n\t\t// Whilst this means some extra code / duplication in this function, it cannot be helped\n\t\t\t\n\t\t// The JSON and Database items being passed in here have passed the following checks:\n\t\t// - skip_file\n\t\t// - skip_dir\n\t\t// - sync_list\n\t\t// - skip_dotfiles\n\t\t// - check_nosync\n\t\t// - skip_size\n\t\t// - Is not currently cached in the local database\n\t\t// As such, we should not be doing any other checks here to determine if the JSON item is wanted .. it is\n\t\t\n\t\tif (exists(newItemPath)) {\n\t\t\tif (debugLogging) {addLogEntry(\"Path on local disk already exists\", [\"debug\"]);}\n\t\t\t// Issue #2209 fix - test if path is a bad symbolic link\n\t\t\tif (isSymlink(newItemPath)) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"Path on local disk is a symbolic link ........\", [\"debug\"]);}\n\t\t\t\tif (!exists(readLink(newItemPath))) {\n\t\t\t\t\t// reading the symbolic link failed\t\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Reading the symbolic link target failed ........ \", [\"debug\"]);}\n\t\t\t\t\taddLogEntry(\"Skipping item - invalid symbolic link: \" ~ newItemPath, [\"info\", \"notify\"]);\n\t\t\t\t\t\n\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// return - invalid symbolic link\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Path exists locally, is not a bad symbolic link\n\t\t\t// Test if this item is actually in-sync\n\t\t\t// What is the source of this item data?\n\t\t\tstring itemSource = \"remote\";\n\t\t\tif (isItemSynced(newDatabaseItem, newItemPath, itemSource)) {\n\t\t\t\t// Issue #3115 - Personal Account Shared Folder\n\t\t\t\t// What account type is this?\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t// Is this a 'remote' DB record\n\t\t\t\t\tif (newDatabaseItem.type == ItemType.remote) {\n\t\t\t\t\t\t// Issue #3136, #3139 #3143\n\t\t\t\t\t\t// Fetch the actual online record for this item\n\t\t\t\t\t\t// This returns the 'actual' OneDrive Personal driveId value and is 15 character checked\n\t\t\t\t\t\tstring actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(newDatabaseItem.remoteDriveId));\n\t\t\t\t\t\tnewDatabaseItem.remoteDriveId = actualOnlineDriveId;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\n\t\t\t\t// Item details from OneDrive and local item details in database are in-sync\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"The item to sync is already present on the local filesystem and is in-sync with what is reported online\", [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"Update/Insert local database with item details: \" ~ to!string(newDatabaseItem), [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Add item to database\n\t\t\t\titemDB.upsert(newDatabaseItem);\n\t\t\t\t\n\t\t\t\t// With the 'newDatabaseItem' saved to the database, regardless of --dry-run situation - was that new database item a 'remote' item?\n\t\t\t\t// If this is this a 'Shared Folder' item - ensure we have created / updated any relevant Database Tie Records\n\t\t\t\t// This should be applicable for all account types\n\t\t\t\tif (newDatabaseItem.type == ItemType.remote) {\n\t\t\t\t\t// yes this is a remote item type\n\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\n\t\t\t\t\tstring relocatedFolderDriveId;\n\t\t\t\t\tstring relocatedFolderParentId;\n\t\t\t\t\t\n\t\t\t\t\t// Is this a relocated Shared Folder? OneDrive Personal and Business supports the relocation of Shared Folder links to other folders\n\t\t\t\t\t// Is this parentId equal to our defaultRootId .. if not it is highly likely that this Shared Folder is in a sub folder in our online folder structure\n\t\t\t\t\tif (newDatabaseItem.parentId != appConfig.defaultRootId) {\n\t\t\t\t\t\t// The parentId is not our defaultRootId .. most likely a relocated shared folder\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"The folder path for this Shared Folder is not our account root, thus is a relocated Shared Folder item. We must pass in the correct parent details for this Shared Folder 'root' object\" , [\"debug\"]);\n\t\t\t\t\t\t\t// What are we setting\n\t\t\t\t\t\t\taddLogEntry(\"Setting relocatedFolderDriveId to:  \" ~ newDatabaseItem.driveId);\n\t\t\t\t\t\t\taddLogEntry(\"Setting relocatedFolderParentId to: \" ~ newDatabaseItem.parentId);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Configure the relocated folders data\n\t\t\t\t\t\trelocatedFolderDriveId = newDatabaseItem.driveId;\n\t\t\t\t\t\trelocatedFolderParentId = newDatabaseItem.parentId;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t// We pass in the JSON element so we can create the right records + if this is a relocated shared folder, give the local parental record identifier\n\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(onedriveJSONItem, relocatedFolderDriveId, relocatedFolderParentId);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Did the user configure to save xattr data about this file?\n\t\t\t\tif (appConfig.getValueBool(\"write_xattr_data\")) {\n\t\t\t\t\twriteXattrData(newItemPath, onedriveJSONItem);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// all done processing this potential new local item\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\t// Item details from OneDrive and local item details in database are NOT in-sync\n\t\t\t\tif (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\"]);}\n\t\t\t\t\n\t\t\t\t// Which object is newer? The local file or the remote file?\n\t\t\t\tSysTime localModifiedTime = timeLastModified(newItemPath).toUTC();\n\t\t\t\tSysTime itemModifiedTime = newDatabaseItem.mtime;\n\t\t\t\t// Reduce time resolution to seconds before comparing\n\t\t\t\tlocalModifiedTime.fracSecs = Duration.zero;\n\t\t\t\titemModifiedTime.fracSecs = Duration.zero;\n\t\t\t\t\n\t\t\t\t// Is the local modified time greater than that from OneDrive?\n\t\t\t\tif (localModifiedTime > itemModifiedTime) {\n\t\t\t\t\t// Local file is newer than item on OneDrive based on file modified time\n\t\t\t\t\t// Is this item id in the database?\n\t\t\t\t\tif (itemDB.idInLocalDatabase(newDatabaseItem.driveId, newDatabaseItem.id)) {\n\t\t\t\t\t\t// item id is in the database\n\t\t\t\t\t\t// no local rename\n\t\t\t\t\t\t// no download needed\n\t\t\t\t\t\t\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\tItem latestDatabaseItem;\n\t\t\t\t\t\titemDB.selectById(newDatabaseItem.driveId, newDatabaseItem.id, latestDatabaseItem);\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"latestDatabaseItem: \" ~ to!string(latestDatabaseItem), [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\tSysTime latestItemModifiedTime = latestDatabaseItem.mtime;\n\t\t\t\t\t\t// Reduce time resolution to seconds before comparing\n\t\t\t\t\t\tlatestItemModifiedTime.fracSecs = Duration.zero;\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (localModifiedTime == latestItemModifiedTime) {\n\t\t\t\t\t\t\t// Log action\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Local file modified time matches existing database record - keeping local file\", [\"verbose\"]);}\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Skipping OneDrive change as this is determined to be unwanted due to local file modified time matching database data\", [\"debug\"]);}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Log action\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Local file modified time is newer based on UTC time conversion - keeping local file as this exists in the local database\", [\"verbose\"]);}\n\t\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Return as no further action needed\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// item id is not in the database .. maybe a --resync ?\n\t\t\t\t\t\t// file exists locally but is not in the sqlite database - maybe a failed download?\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Local item does not exist in local database - replacing with file from OneDrive - failed download?\", [\"verbose\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\t// To this pint we have passed the following checks:\n\t\t\t\t\t\t// 1. Any client side filtering checks - this determined this is a file that is wanted\n\t\t\t\t\t\t// 2. A file with the exact name exists locally\n\t\t\t\t\t\t// 3. The local modified time > remote modified time\n\t\t\t\t\t\t// 4. The id of the item from OneDrive is not in the database\n\t\t\t\t\t\t\n\t\t\t\t\t\t// If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not\n\t\t\t\t\t\t// In case the renamed path is needed\n\t\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t\tsafeBackup(newItemPath, dryRun, bypassDataPreservation, renamedPath);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Is the remote newer?\n\t\t\t\t\tif (localModifiedTime < itemModifiedTime) {\n\t\t\t\t\t\t// Remote file is newer than the existing local item\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Remote item modified time is newer based on UTC time conversion\", [\"verbose\"]);} // correct message, remote item is newer\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"localModifiedTime (local file):   \" ~ to!string(localModifiedTime), [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"itemModifiedTime (OneDrive item): \" ~ to!string(itemModifiedTime), [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is this the exact same file?\n\t\t\t\t\t\t// Test the file hash\n\t\t\t\t\t\tif (!testFileHash(newItemPath, newDatabaseItem)) {\n\t\t\t\t\t\t\t// File on disk is different by hash / content\n\t\t\t\t\t\t\t// If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not\n\t\t\t\t\t\t\t// In case the renamed path is needed\n\t\t\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t\t\tsafeBackup(newItemPath, dryRun, bypassDataPreservation, renamedPath);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// File on disk is the same by hash / content, but is a different timestamp\n\t\t\t\t\t\t\t// The file contents have not changed, but the modified timestamp has\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The last modified timestamp online has changed however the local file content has not changed\", [\"verbose\"]);}\n\t\t\t\t\t\t\t// Update the local timestamp, logging and error handling done within function\n\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, newItemPath, newDatabaseItem.mtime);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Are the timestamps equal?\n\t\t\t\t\tif (localModifiedTime == itemModifiedTime) {\n\t\t\t\t\t\t// yes they are equal\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"File timestamps are equal, no further action required\", [\"debug\"]); // correct message as timestamps are equal\n\t\t\t\t\t\t\taddLogEntry(\"Update/Insert local database with item details: \" ~ to!string(newDatabaseItem), [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Add item to database\n\t\t\t\t\t\titemDB.upsert(newDatabaseItem);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Did the user configure to save xattr data about this file?\n\t\t\t\t\t\tif (appConfig.getValueBool(\"write_xattr_data\")) {\n\t\t\t\t\t\t\twriteXattrData(newItemPath, onedriveJSONItem);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// everything all OK, DB updated\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} \n\t\t\t\n\t\t// Path does not exist locally (should not exist locally if renamed file) - this will be a new file download or new folder creation\n\t\t// How to handle this Potentially New Local Item JSON ?\n\t\tfinal switch (newDatabaseItem.type) {\n\t\t\tcase ItemType.file:\n\t\t\t\t// Add to the file to the download array for processing later\n\t\t\t\tfileJSONItemsToDownload ~= onedriveJSONItem;\n\t\t\t\tgoto functionCompletion;\n\t\t\t\t\n\t\t\tcase ItemType.dir:\n\t\t\t\t// Create the directory immediately as we depend on its entry existing\n\t\t\t\thandleLocalDirectoryCreation(newDatabaseItem, newItemPath, onedriveJSONItem);\n\t\t\t\tgoto functionCompletion;\n\t\t\t\t\n\t\t\tcase ItemType.remote:\n\t\t\t\t// Add to the directory and relevant details for processing later\n\t\t\t\tif (newDatabaseItem.remoteType == ItemType.dir) {\n\t\t\t\t\thandleLocalDirectoryCreation(newDatabaseItem, newItemPath, onedriveJSONItem);\n\t\t\t\t} else {\n\t\t\t\t\t// Add to the file to the download array for processing later\n\t\t\t\t\tfileJSONItemsToDownload ~= onedriveJSONItem;\n\t\t\t\t}\n\t\t\t\tgoto functionCompletion;\n\t\t\t\t\n\t\t\tcase ItemType.root:\n\t\t\tcase ItemType.unknown:\n\t\t\tcase ItemType.none:\n\t\t\t\t// Unknown type - we dont action or sync these items\n\t\t\t\tgoto functionCompletion;\n\t\t}\n\t\t\n\t\t// 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\n\t\tfunctionCompletion:\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t}\n\t\n\t// Handle the creation of a new local directory\n\tvoid handleLocalDirectoryCreation(Item newDatabaseItem, string newItemPath, JSONValue onedriveJSONItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// To create a path, 'newItemPath' must not be empty\n\t\tif (!newItemPath.empty) {\n\t\t\t// Update the logging output to be consistent\n\t\t\tif (verboseLogging) {addLogEntry(\"Creating local directory: \" ~ \"./\" ~ buildNormalizedPath(newItemPath), [\"verbose\"]);}\n\t\t\tif (!dryRun) {\n\t\t\t\ttry {\n\t\t\t\t\t// Create the new directory\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Requested local path does not exist, creating directory structure: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\tmkdirRecurse(newItemPath);\n\t\t\t\t\t\n\t\t\t\t\t// Has the user disabled the setting of filesystem permissions?\n\t\t\t\t\tif (!appConfig.getValueBool(\"disable_permission_set\")) {\n\t\t\t\t\t\t// Configure the applicable permissions for the folder\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting directory permissions for: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\t\tnewItemPath.setAttributes(appConfig.returnRequiredDirectoryPermissions());\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Use inherited permissions\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Using inherited filesystem permissions for: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Update the time of the folder to match the last modified time as is provided by OneDrive\n\t\t\t\t\t// If there are any files then downloaded into this folder, the last modified time will get \n\t\t\t\t\t// updated by the local Operating System with the latest timestamp - as this is normal operation\n\t\t\t\t\t// as the directory has been modified\n\t\t\t\t\t// Set the timestamp, logging and error handling done within function\n\t\t\t\t\tsetLocalPathTimestamp(dryRun, newItemPath, newDatabaseItem.mtime);\n\t\t\t\t\t\n\t\t\t\t\t// Save the newDatabaseItem to the database\n\t\t\t\t\tsaveDatabaseItem(newDatabaseItem);\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// display the error message\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, newItemPath);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// we dont create the directory, but we need to track that we 'faked it'\n\t\t\t\tidsFaked ~= [newDatabaseItem.driveId, newDatabaseItem.id];\n\t\t\t\t// Save the newDatabaseItem to the database\n\t\t\t\tsaveDatabaseItem(newDatabaseItem);\n\t\t\t}\n\t\t\t\n\t\t\t// With the 'newDatabaseItem' saved to the database, regardless of --dry-run situation - was that new database item a 'remote' item?\n\t\t\t// Is this folder that has been created locally a 'Shared Folder' online?\n\t\t\t// This should be applicable for all account types\n\t\t\tif (newDatabaseItem.type == ItemType.remote) {\n\t\t\t\t// yes this is a remote item type\n\t\t\t\tif (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\"]);}\n\t\t\t\t\n\t\t\t\tstring relocatedFolderDriveId;\n\t\t\t\tstring relocatedFolderParentId;\n\t\t\t\t\n\t\t\t\t// Is this a relocated Shared Folder? OneDrive Personal and Business supports the relocation of Shared Folder links to other folders\n\t\t\t\t// Is this parentId equal to our defaultRootId .. if not it is highly likely that this Shared Folder is in a sub folder in our online folder structure\n\t\t\t\tif (newDatabaseItem.parentId != appConfig.defaultRootId) {\n\t\t\t\t\t// The parentId is not our defaultRootId .. most likely a relocated shared folder\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"The folder path for this Shared Folder is not our account root, thus is a relocated Shared Folder item. We must pass in the correct parent details for this Shared Folder 'root' object\" , [\"debug\"]);\n\t\t\t\t\t\t// What are we setting\n\t\t\t\t\t\taddLogEntry(\"Setting relocatedFolderDriveId to:  \" ~ newDatabaseItem.driveId);\n\t\t\t\t\t\taddLogEntry(\"Setting relocatedFolderParentId to: \" ~ newDatabaseItem.parentId);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Configure the relocated folders data\n\t\t\t\t\trelocatedFolderDriveId = newDatabaseItem.driveId;\n\t\t\t\t\trelocatedFolderParentId = newDatabaseItem.parentId;\n\t\t\t\t}\n\t\t\t\n\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t// We pass in the JSON element so we can create the right records + if this is a relocated shared folder, give the local parental record identifier\n\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(onedriveJSONItem, relocatedFolderDriveId, relocatedFolderParentId);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Create 'root' DB Tie Record and 'Shared Folder' DB Record in a consistent manner\n\tvoid createRequiredSharedFolderDatabaseRecords(JSONValue onedriveJSONItem, string relocatedFolderDriveId = null, string relocatedFolderParentId = null) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Due to this function, we need to keep the return code, so that this function operates as efficiently as possible.\n\t\t// Whilst this means some extra code / duplication in this function, it cannot be helped\n\t\n\t\t// Detail what we are doing\n\t\tif (debugLogging) {addLogEntry(\"We have been requested to create 'root' and 'Shared Folder' DB Tie Records in a consistent manner\" , [\"debug\"]);}\n\t\t\n\t\tJSONValue onlineParentData;\n\t\tstring parentDriveId;\n\t\tstring parentObjectId;\n\t\tOneDriveApi onlineParentOneDriveApiInstance;\n\t\tonlineParentOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tonlineParentOneDriveApiInstance.initialise();\n\t\t\n\t\t// Using the onlineParentData JSON data make a DB record for this parent item so that it exists in the database\n\t\tItem sharedFolderDatabaseTie;\n\t\t\n\t\t// A Shared Folder should have [\"remoteItem\"][\"parentReference\"] elements\n\t\tbool remoteItemElementsExist = false;\n\t\t\n\t\t// Test that the required elements exist for Shared Folder DB entry creations to occur\n\t\tif (isItemRemote(onedriveJSONItem)) {\n\t\t\t// Required [\"remoteItem\"] element exists in the JSON data\n\t\t\tif ((hasRemoteParentDriveId(onedriveJSONItem)) && (hasRemoteItemId(onedriveJSONItem))) {\n\t\t\t\t// Required elements exist\n\t\t\t\tremoteItemElementsExist = true;\n\t\t\t\t// What account type is this? This needs to be configured correctly so this can be queried correctly\n\t\t\t\t// - The setting of this is the 'same' for account types, but previously this was shown to need different data. Future code optimisation potentially here.\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t// OneDrive Personal JSON has this structure that we need to use\n\t\t\t\t\tparentDriveId = onedriveJSONItem[\"remoteItem\"][\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tparentObjectId = onedriveJSONItem[\"remoteItem\"][\"id\"].str;\n\t\t\t\t} else {\n\t\t\t\t\t// OneDrive Business|Sharepoint JSON has this structure that we need to use\n\t\t\t\t\tparentDriveId = onedriveJSONItem[\"remoteItem\"][\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tparentObjectId = onedriveJSONItem[\"remoteItem\"][\"id\"].str;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// If the required elements do not exist, the Shared Folder DB elements cannot be created\n\t\tif (!remoteItemElementsExist) {\n\t\t\t// We cannot create the required entries in the database\n\t\t\tif (debugLogging) {addLogEntry(\"Unable to create 'root' and 'Shared Folder' DB Tie Records in a consistent manner - required elements missing from provided JSON record\" , [\"debug\"]);}\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Issue #3115 - Validate 'parentDriveId' length\n\t\t// What account type is this?\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\tparentDriveId = transformToLowerCase(parentDriveId);\n\t\t\t\n\t\t\t// Test if the 'parentDriveId' is not equal to appConfig.defaultDriveId\n\t\t\tif (parentDriveId != appConfig.defaultDriveId) {\n\t\t\t\t// Test 'parentDriveId' for length and validation - 15 character API bug\n\t\t\t\tparentDriveId = testProvidedDriveIdForLengthIssue(parentDriveId);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Try and fetch this shared folder parent's details\n\t\ttry {\n\t\t\tif (debugLogging) {addLogEntry(format(\"Fetching Shared Folder online data for parentDriveId '%s' and parentObjectId '%s'\", parentDriveId, parentObjectId), [\"debug\"]);}\n\t\t\tonlineParentData = onlineParentOneDriveApiInstance.getPathDetailsById(parentDriveId, parentObjectId);\n\t\t} catch (OneDriveException exception) {\n\t\t\t// 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?\n\t\t\tif ((exception.httpStatusCode == 403) || (exception.httpStatusCode == 404)) {\n\t\t\t\t// The API call returned a 404 error response\n\t\t\t\tif (debugLogging) {addLogEntry(\"onlineParentData = onlineParentOneDriveApiInstance.getPathDetailsById(parentDriveId, parentObjectId); generated a 404 - shared folder path does not exist online\", [\"debug\"]);}\n\t\t\t\tstring errorMessage = format(\"WARNING: The OneDrive Shared Folder link target '%s' cannot be found online using the provided online data.\", onedriveJSONItem[\"name\"].str);\n\t\t\t\t// detail what this 404 error response means\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(errorMessage);\n\t\t\t\taddLogEntry(\"WARNING: This is potentially a broken online OneDrive Shared Folder link or you no longer have access to it. Please correct this error online.\");\n\t\t\t\taddLogEntry();\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tonlineParentOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\tonlineParentOneDriveApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// we have to return at this point\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\t// Catch all other errors\n\t\t\t\t// Display what the error is\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tonlineParentOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\tonlineParentOneDriveApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If we get an error, we cannot do much else\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Create a 'root' DB Tie Record for a Shared Folder from the parent folder JSON data\n\t\t// - 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\n\t\tcreateDatabaseRootTieRecordForOnlineSharedFolder(onlineParentData, relocatedFolderDriveId, relocatedFolderParentId);\n\t\t\n\t\t// Log that we are created the Shared Folder Tie record now\n\t\tif (debugLogging) {addLogEntry(\"Creating the Shared Folder DB Tie Record that binds the 'root' record to the 'shared folder'\" , [\"debug\"]);}\n\t\t\n\t\t// Make an item from the online JSON data\n\t\tsharedFolderDatabaseTie = makeItem(onlineParentData);\n\t\t// Ensure we use our online name, as we may have renamed the folder in our location\n\t\tsharedFolderDatabaseTie.name = onedriveJSONItem[\"name\"].str; // use this as the name .. this is the name of the folder online in our OneDrive account, not the online parent name\n\t\t\n\t\t// Is sharedFolderDatabaseTie.driveId empty?\n\t\tif (sharedFolderDatabaseTie.driveId.empty) {\n\t\t\t// This cannot be empty - set to the correct reference for the Shared Folder DB Tie record\n\t\t\tif (debugLogging) {addLogEntry(\"The Shared Folder DB Tie record entry for 'driveId' is empty ... correcting it\" , [\"debug\"]);}\n\t\t\tsharedFolderDatabaseTie.driveId = onlineParentData[\"parentReference\"][\"driveId\"].str;\n\t\t}\n\t\t\n\t\t// Ensure 'parentId' is not empty, except for Personal Accounts\n\t\tif (appConfig.accountType != \"personal\") {\n\t\t\t// Is sharedFolderDatabaseTie.parentId.empty?\n\t\t\tif (sharedFolderDatabaseTie.parentId.empty) {\n\t\t\t\t// This cannot be empty - set to the correct reference for the Shared Folder DB Tie record\n\t\t\t\tif (debugLogging) {addLogEntry(\"The Shared Folder DB Tie record entry for 'parentId' is empty ... correcting it\" , [\"debug\"]);}\n\t\t\t\tsharedFolderDatabaseTie.parentId = onlineParentData[\"id\"].str;\n\t\t\t}\n\t\t} else {\n\t\t\t// The database Tie Record for Personal Accounts must be empty .. no change, leave 'parentId' empty\n\t\t}\n\t\t\n\t\t// If a user has added the 'whole' SharePoint Document Library, then the DB Shared Folder Tie Record and 'root' record are the 'same'\n\t\tif ((isItemRoot(onlineParentData)) && (onlineParentData[\"parentReference\"][\"driveType\"].str == \"documentLibrary\")) {\n\t\t\t// Yes this is a DocumentLibrary 'root' object\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"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\"]);\n\t\t\t\taddLogEntry(\" sharedFolderDatabaseTie.parentId = null\", [\"debug\"]);\n\t\t\t\taddLogEntry(\" sharedFolderDatabaseTie.type = ItemType.root\", [\"debug\"]);\n\t\t\t}\n\t\t\tsharedFolderDatabaseTie.parentId = null;\n\t\t\tsharedFolderDatabaseTie.type = ItemType.root;\n\t\t}\n\t\t\n\t\t// Personal Account Shared Folder Handling \n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Yes this is a personal account\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"Updating Shared Folder DB Tie record entry with correct type value as this as it is a Personal Shared Folder Object\" , [\"debug\"]);\n\t\t\t\taddLogEntry(\" sharedFolderDatabaseTie.type = ItemType.dir\", [\"debug\"]);\n\t\t\t}\n\t\t\tsharedFolderDatabaseTie.type = ItemType.dir;\n\t\t}\n\t\t\n\t\t// Issue #3115 - Validate sharedFolderDatabaseTie.driveId length\n\t\t// What account type is this?\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\tsharedFolderDatabaseTie.driveId = transformToLowerCase(sharedFolderDatabaseTie.driveId);\n\t\t\t\n\t\t\t// Test sharedFolderDatabaseTie.driveId length and validation if the sharedFolderDatabaseTie.driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\tif (sharedFolderDatabaseTie.driveId != appConfig.defaultDriveId) {\n\t\t\t\tsharedFolderDatabaseTie.driveId = testProvidedDriveIdForLengthIssue(sharedFolderDatabaseTie.driveId);\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t\t// Log action\n\t\taddLogEntry(\"Creating|Updating a DB Tie Record for this Shared Folder from the online parental data: \" ~ sharedFolderDatabaseTie.name, [\"debug\"]);\n\t\taddLogEntry(\"Shared Folder DB Tie Record data: \" ~ to!string(sharedFolderDatabaseTie), [\"debug\"]);\n\t\t\n\t\t// Is this a dry-run excercise?\n\t\tif (dryRun) {\n\t\t\t// We need to ensure we add this to our faked entries\n\t\t\tidsFaked ~= [sharedFolderDatabaseTie.driveId, sharedFolderDatabaseTie.id];\n\t\t}\n\t\t\n\t\t// Save item\n\t\titemDB.upsert(sharedFolderDatabaseTie);\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tonlineParentOneDriveApiInstance.releaseCurlEngine();\n\t\tonlineParentOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\t\t\n\t}\n\t\n\t// If the JSON item IS in the database, this will be an update to an existing in-sync item\n\tvoid applyPotentiallyChangedItem(Item existingDatabaseItem, string existingItemPath, Item changedOneDriveItem, string changedItemPath, JSONValue onedriveJSONItem) {\n\t\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\t\n\t\t// If we are moving the item, we do not need to download it again\n\t\tbool itemWasMoved = false;\n\t\t\n\t\t// Do we need to actually update the database with the details that were provided by the OneDrive API?\n\t\t// Calculate these time items from the provided items\n\t\tSysTime existingItemModifiedTime = existingDatabaseItem.mtime;\n\t\texistingItemModifiedTime.fracSecs = Duration.zero;\n\t\tSysTime changedOneDriveItemModifiedTime = changedOneDriveItem.mtime;\n\t\tchangedOneDriveItemModifiedTime.fracSecs = Duration.zero;\n\t\t\n\t\t// Did the eTag change?\n\t\tif (existingDatabaseItem.eTag != changedOneDriveItem.eTag) {\n\t\t\t// The eTag has changed to what we previously cached\n\t\t\tif (existingItemPath != changedItemPath) {\n\t\t\t\t// Log that we are changing / moving an item to a new name\n\t\t\t\taddLogEntry(\"Moving \" ~ existingItemPath ~ \" to \" ~ changedItemPath);\n\t\t\t\t// Is the destination path empty .. or does something exist at that location?\n\t\t\t\tif (exists(changedItemPath)) {\n\t\t\t\t\t// Destination we are moving to exists ... \n\t\t\t\t\tItem changedLocalItem;\n\t\t\t\t\t// Query DB for this changed item in specified path that exists and see if it is in-sync\n\t\t\t\t\tif (itemDB.selectByPath(changedItemPath, changedOneDriveItem.driveId, changedLocalItem)) {\n\t\t\t\t\t\t// The 'changedItemPath' is in the database\n\t\t\t\t\t\tstring itemSource = \"database\";\n\t\t\t\t\t\tif (isItemSynced(changedLocalItem, changedItemPath, itemSource)) {\n\t\t\t\t\t\t\t// The destination item is in-sync\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Destination is in sync and will be overwritten\", [\"verbose\"]);}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// The destination item is different\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The destination is occupied with a different item, renaming the conflicting file...\", [\"verbose\"]);}\n\t\t\t\t\t\t\t// If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not\n\t\t\t\t\t\t\t// In case the renamed path is needed\n\t\t\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t\t\tsafeBackup(changedItemPath, dryRun, bypassDataPreservation, renamedPath);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// The to be overwritten item is not already in the itemdb, so it should saved to avoid data loss\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The destination is occupied by an existing un-synced file, renaming the conflicting file...\", [\"verbose\"]);}\n\t\t\t\t\t\t// If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not\n\t\t\t\t\t\t// In case the renamed path is needed\n\t\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t\tsafeBackup(changedItemPath, dryRun, bypassDataPreservation, renamedPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// We should no longer need a try block for safeRename() as retry / error handling occurs within safeRename() and setLocalPathTimestamp() .. but keeping this for the moment\n\t\t\t\ttry {\n\t\t\t\t\t// If we are in a --dry-run situation?\n\t\t\t\t\tif(!dryRun) {\n\t\t\t\t\t\t// We are not in a --dry-run situation\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Attempt rename (returns true only if rename succeeded)\n\t\t\t\t\t\tbool renamedOk = safeRename(existingItemPath, changedItemPath, dryRun);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Was the rename successful?\n\t\t\t\t\t\tif (renamedOk) {\n\t\t\t\t\t\t\t// Flag that the item was moved | renamed\n\t\t\t\t\t\t\titemWasMoved = true;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// If the item is a file, make sure that the local timestamp now is the same as the timestamp online\n\t\t\t\t\t\t\t// Otherwise when we do the DB check, the move on the file system, the file technically has a newer timestamp\n\t\t\t\t\t\t\t// which is 'correct' .. but we need to report locally the online timestamp here as the move was made online\n\t\t\t\t\t\t\tif (changedOneDriveItem.type == ItemType.file) {\n\t\t\t\t\t\t\t\t// Set the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, changedItemPath, changedOneDriveItem.mtime);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Rename failed - do NOT track as moved, do NOT touch timestamps on the target path\n\t\t\t\t\t\t\taddLogEntry(\"ERROR: Local rename failed; item will not be treated as moved: \" ~ to!string(existingItemPath) ~ \" -> \" ~ to!string(changedItemPath), [\"error\", \"notify\"]);\n\t\t\t\t\t\t\t// We need to return here and stop processing this JSON item ... \n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// --dry-run situation - the actual rename did not occur - but we need to track like it did\n\t\t\t\t\t\t// Track this as a faked id item\n\t\t\t\t\t\tidsFaked ~= [changedOneDriveItem.driveId, changedOneDriveItem.id];\n\t\t\t\t\t\t// We also need to track that we did not rename this path\n\t\t\t\t\t\t// When we are checking entries in this array, paths need to have './' added\n\t\t\t\t\t\tpathsRenamed ~= [ensureStartsWithDotSlash(buildNormalizedPath(existingItemPath))];\n\t\t\t\t\t}\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// Display the error message from the filesystem\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, existingItemPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// What sort of changed item is this?\n\t\t\t// Is it a file or remote file, and we did not move it ..\n\t\t\tif (((changedOneDriveItem.type == ItemType.file) && (!itemWasMoved)) || (((changedOneDriveItem.type == ItemType.remote) && (changedOneDriveItem.remoteType == ItemType.file)) && (!itemWasMoved))) {\n\t\t\t\t// The eTag is notorious for being 'changed' online by some backend Microsoft process\n\t\t\t\tif (existingDatabaseItem.quickXorHash != changedOneDriveItem.quickXorHash) {\n\t\t\t\t\t// Add to the items to download array for processing - the file hash we previously recorded is not the same as online\n\t\t\t\t\tfileJSONItemsToDownload ~= onedriveJSONItem;\n\t\t\t\t} else {\n\t\t\t\t\t// 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\n\t\t\t\t\t// 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\n\t\t\t\t\t// 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\n\t\t\t\t\t// as all records are touched / updated when performing the OneDrive sync operations. The impacted operational modes are:\n\t\t\t\t\t// - National Cloud Deployments do not support /delta as a query\n\t\t\t\t\t// - When using --single-directory\n\t\t\t\t\t// - When using --download-only --cleanup-local-files\n\t\t\t\t\n\t\t\t\t\t// 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?\n\t\t\t\t\tif ((existingItemModifiedTime != changedOneDriveItemModifiedTime) || (generateSimulatedDeltaResponse)) {\n\t\t\t\t\t\t// Save this item in the database\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Issue #3115 - Personal Account Shared Folder\n\t\t\t\t\t\t// What account type is this?\n\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t// Is this a 'remote' DB record\n\t\t\t\t\t\t\tif (changedOneDriveItem.type == ItemType.remote) {\n\t\t\t\t\t\t\t\t// Issue #3136, #3139 #3143\n\t\t\t\t\t\t\t\t// Fetch the actual online record for this item\n\t\t\t\t\t\t\t\t// This returns the actual OneDrive Personal driveId value and is 15 character checked\n\t\t\t\t\t\t\t\tstring actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(changedOneDriveItem.remoteDriveId));\n\t\t\t\t\t\t\t\tchangedOneDriveItem.remoteDriveId = actualOnlineDriveId;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Add to the local database\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Adding changed OneDrive Item to database: \" ~ to!string(changedOneDriveItem), [\"debug\"]);}\n\t\t\t\t\t\titemDB.upsert(changedOneDriveItem);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Save this item in the database\n\t\t\t\tsaveItem(onedriveJSONItem);\n\t\t\t\t\n\t\t\t\t// If the 'Add shortcut to My files' link was the item that was actually renamed .. we have to update our DB records\n\t\t\t\tif (changedOneDriveItem.type == ItemType.remote) {\n\t\t\t\t\t// Select remote item data from the database\n\t\t\t\t\tItem existingRemoteDbItem;\n\t\t\t\t\titemDB.selectById(changedOneDriveItem.remoteDriveId, changedOneDriveItem.remoteId, existingRemoteDbItem);\n\t\t\t\t\t// Update the 'name' in existingRemoteDbItem and save it back to the database\n\t\t\t\t\t// This is the local name stored on disk that was just 'moved'\n\t\t\t\t\texistingRemoteDbItem.name = changedOneDriveItem.name;\n\t\t\t\t\titemDB.upsert(existingRemoteDbItem);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// The existingDatabaseItem.eTag == changedOneDriveItem.eTag .. nothing has changed eTag wise\n\t\t\t\n\t\t\t// 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\n\t\t\t// 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\n\t\t\t// 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\n\t\t\t// as all records are touched / updated when performing the OneDrive sync operations. The impacted operational modes are:\n\t\t\t// - National Cloud Deployments do not support /delta as a query\n\t\t\t// - When using --single-directory\n\t\t\t// - When using --download-only --cleanup-local-files\n\t\t\n\t\t\t// 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?\n\t\t\tif ((existingItemModifiedTime != changedOneDriveItemModifiedTime) || (generateSimulatedDeltaResponse)) {\n\t\t\t\t// Database update needed for this item because our local record is out-of-date\n\t\t\t\t\n\t\t\t\t// Issue #3115 - Personal Account Shared Folder\n\t\t\t\t// What account type is this?\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t// Is this a 'remote' DB record\n\t\t\t\t\tif (changedOneDriveItem.type == ItemType.remote) {\n\t\t\t\t\t\t// Issue #3136, #3139 #3143\n\t\t\t\t\t\t// Fetch the actual online record for this item\n\t\t\t\t\t\t// This returns the actual OneDrive Personal driveId value and is 15 character checked\n\t\t\t\t\t\tstring actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(changedOneDriveItem.remoteDriveId));\n\t\t\t\t\t\tchangedOneDriveItem.remoteDriveId = actualOnlineDriveId;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Add to the local database\n\t\t\t\tif (debugLogging) {addLogEntry(\"Adding changed OneDrive Item to database: \" ~ to!string(changedOneDriveItem), [\"debug\"]);}\n\t\t\t\titemDB.upsert(changedOneDriveItem);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Download new/changed file items as identified\n\tvoid downloadOneDriveItems() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Lets deal with all the JSON items that need to be downloaded in a batch process\n\t\tsize_t batchSize = to!int(appConfig.getValueLong(\"threads\"));\n\t\tlong batchCount = (fileJSONItemsToDownload.length + batchSize - 1) / batchSize;\n\t\tlong batchesProcessed = 0;\n\t\t\n\t\t// Transfer order\n\t\tstring transferOrder = appConfig.getValueString(\"transfer_order\");\n\t\t\n\t\t// Has the user configured to specify the transfer order of files?\n\t\tif (transferOrder != \"default\") {\n\t\t\t// If we have more than 1 item to download, sort the items\n\t\t\tif (count(fileJSONItemsToDownload) > 1) {\n\t\t\t\n\t\t\t\t// Perform sorting based on transferOrder\n\t\t\t\tif (transferOrder == \"size_asc\") {\n\t\t\t\t\tfileJSONItemsToDownload.sort!((a, b) => a[\"size\"].integer < b[\"size\"].integer); // sort the array by ascending size\n\t\t\t\t} else if (transferOrder == \"size_dsc\") {\n\t\t\t\t\tfileJSONItemsToDownload.sort!((a, b) => a[\"size\"].integer > b[\"size\"].integer); // sort the array by descending size\n\t\t\t\t} else if (transferOrder == \"name_asc\") {\n\t\t\t\t\tfileJSONItemsToDownload.sort!((a, b) => a[\"name\"].str < b[\"name\"].str); // sort the array by ascending name\n\t\t\t\t} else if (transferOrder == \"name_dsc\") {\n\t\t\t\t\tfileJSONItemsToDownload.sort!((a, b) => a[\"name\"].str > b[\"name\"].str); // sort the array by descending name\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Process fileJSONItemsToDownload\n\t\tforeach (chunk; fileJSONItemsToDownload.chunks(batchSize)) {\n\t\t\t// send an array containing 'appConfig.getValueLong(\"threads\")' JSON items to download\n\t\t\tdownloadOneDriveItemsInParallel(chunk);\n\t\t}\n\t\t\n\t\t// For this set of items, perform a DB PASSIVE checkpoint\n\t\titemDB.performCheckpoint(\"PASSIVE\");\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Download items in parallel\n\tvoid downloadOneDriveItemsInParallel(JSONValue[] array) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// This function received an array of JSON items to download, the number of elements based on appConfig.getValueLong(\"threads\")\n\t\tforeach (i, onedriveJSONItem; processPool.parallel(array)) {\n\t\t\t// Take each JSON item and download it\n\t\t\tdownloadFileItem(onedriveJSONItem);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Perform the actual download of an object from OneDrive\n\tvoid downloadFileItem(JSONValue onedriveJSONItem, bool ignoreDataPreservationCheck = false, long resumeOffset = 0) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Function variables\t\t\n\t\tbool downloadFailed = false;\n\t\tstring OneDriveFileXORHash;\n\t\tstring OneDriveFileSHA256Hash;\n\t\tlong jsonFileSize = 0;\n\t\tItem databaseItem;\n\t\tbool fileFoundInDB = false;\n\t\t\n\t\t// Create a JSONValue to store the online hash for resumable file checking\n\t\tJSONValue onlineHash;\n\t\t\n\t\t// Capture what time this download started\n\t\tSysTime downloadStartTime = Clock.currTime();\n\t\t\n\t\t// Download item specifics\n\t\tstring downloadItemId = onedriveJSONItem[\"id\"].str;\n\t\tstring downloadItemName = onedriveJSONItem[\"name\"].str;\n\t\tstring downloadDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\tstring downloadParentId = onedriveJSONItem[\"parentReference\"][\"id\"].str;\n\t\t\n\t\t// Calculate this items path\n\t\tstring newItemPath = computeItemPath(downloadDriveId, downloadParentId) ~ \"/\" ~ downloadItemName;\n\t\tif (debugLogging) {addLogEntry(\"JSON Item calculated full path for download is: \" ~ newItemPath, [\"debug\"]);}\n\t\t\n\t\t// Is the item reported as Malware ?\n\t\tif (isMalware(onedriveJSONItem)){\n\t\t\t// OneDrive reports that this file is malware\n\t\t\taddLogEntry(\"ERROR: MALWARE DETECTED IN FILE - DOWNLOAD SKIPPED: \" ~ newItemPath, [\"info\", \"notify\"]);\n\t\t\tdownloadFailed = true;\n\t\t} else {\n\t\t\t// Grab this file's filesize\n\t\t\tif (hasFileSize(onedriveJSONItem)) {\n\t\t\t\t// Use the configured filesize as reported by OneDrive\n\t\t\t\tjsonFileSize = onedriveJSONItem[\"size\"].integer;\n\t\t\t} else {\n\t\t\t\t// filesize missing\n\t\t\t\tif (debugLogging) {addLogEntry(\"ERROR: onedriveJSONItem['size'] is missing\", [\"debug\"]);}\n\t\t\t}\n\t\t\t\n\t\t\t// Configure the hashes for comparison post download\n\t\t\tif (hasHashes(onedriveJSONItem)) {\n\t\t\t\t// File details returned hash details\n\t\t\t\t// QuickXorHash\n\t\t\t\tif (hasQuickXorHash(onedriveJSONItem)) {\n\t\t\t\t\t// Use the provided quickXorHash as reported by OneDrive\n\t\t\t\t\tif (onedriveJSONItem[\"file\"][\"hashes\"][\"quickXorHash\"].str != \"\") {\n\t\t\t\t\t\tOneDriveFileXORHash = onedriveJSONItem[\"file\"][\"hashes\"][\"quickXorHash\"].str;\n\t\t\t\t\t}\n\t\t\t\t\t// Assign to JSONValue as object for resumable file checking\n\t\t\t\t\tonlineHash = JSONValue([\n\t\t\t\t\t\t\"quickXorHash\": JSONValue(OneDriveFileXORHash)\n\t\t\t\t\t]);\n\t\t\t\t} else {\n\t\t\t\t\t// Fallback: Check for SHA256Hash\n\t\t\t\t\tif (hasSHA256Hash(onedriveJSONItem)) {\n\t\t\t\t\t\t// Use the provided sha256Hash as reported by OneDrive\n\t\t\t\t\t\tif (onedriveJSONItem[\"file\"][\"hashes\"][\"sha256Hash\"].str != \"\") {\n\t\t\t\t\t\t\tOneDriveFileSHA256Hash = onedriveJSONItem[\"file\"][\"hashes\"][\"sha256Hash\"].str;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Assign to JSONValue as object for resumable file checking\n\t\t\t\t\t\tonlineHash = JSONValue([\n\t\t\t\t\t\t\t\"sha256Hash\": JSONValue(OneDriveFileSHA256Hash)\n\t\t\t\t\t\t]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// file hash data missing\n\t\t\t\tif (debugLogging) {addLogEntry(\"ERROR: onedriveJSONItem['file']['hashes'] is missing - unable to compare file hash after download to verify integrity of the downloaded file\", [\"debug\"]);}\n\t\t\t\t// Assign to JSONValue as object for resumable file checking\n\t\t\t\tonlineHash = JSONValue([\n\t\t\t\t\t\t\t\"hashMissing\": JSONValue(\"none\")\n\t\t\t\t\t\t]);\n\t\t\t}\n\t\t\n\t\t\t// Does the file already exist in the path locally?\n\t\t\tif (exists(newItemPath)) {\n\t\t\t\t// 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\n\t\t\t\tif (!ignoreDataPreservationCheck) {\n\t\t\t\n\t\t\t\t\t// file exists locally already\n\t\t\t\t\tforeach (driveId; onlineDriveDetails.keys) {\n\t\t\t\t\t\tif (itemDB.selectByPath(newItemPath, driveId, databaseItem)) {\n\t\t\t\t\t\t\tfileFoundInDB = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Log the DB details\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"File to download exists locally and this is the DB record: \" ~ to!string(databaseItem), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Does the DB (what we think is in sync) hash match the existing local file hash?\n\t\t\t\t\tif (!testFileHash(newItemPath, databaseItem)) {\n\t\t\t\t\t\t// local file is different to what we know to be true\n\t\t\t\t\t\taddLogEntry(\"The local file to replace (\" ~ newItemPath ~ \") has been modified locally since the last download. Renaming it to avoid potential local data loss.\");\n\t\t\t\t\t\t// If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not\n\t\t\t\t\t\t// In case the renamed path is needed\n\t\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t\tsafeBackup(newItemPath, dryRun, bypassDataPreservation, renamedPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Is there enough free space locally to download the file\n\t\t\t// - We can use '.' here as we change the current working directory to the configured 'sync_dir'\n\t\t\tlong localActualFreeSpace = to!long(getAvailableDiskSpace(\".\"));\n\t\t\t// 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\n\t\t\t// The reservation value is user configurable in the config file, 50MB by default\n\t\t\tlong freeSpaceReservation = appConfig.getValueLong(\"space_reservation\");\n\t\t\t// debug output\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"Local Disk Space Actual: \" ~ to!string(localActualFreeSpace), [\"debug\"]);\n\t\t\t\taddLogEntry(\"Free Space Reservation:  \" ~ to!string(freeSpaceReservation), [\"debug\"]);\n\t\t\t\taddLogEntry(\"File Size to Download:   \" ~ to!string(jsonFileSize), [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Calculate if we can actually download file - is there enough free space?\n\t\t\tif ((localActualFreeSpace < freeSpaceReservation) || (jsonFileSize > localActualFreeSpace)) {\n\t\t\t\t// localActualFreeSpace is less than freeSpaceReservation .. insufficient free space\n\t\t\t\t// jsonFileSize is greater than localActualFreeSpace .. insufficient free space\n\t\t\t\taddLogEntry(\"Downloading file: \" ~ newItemPath ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry(\"Insufficient local disk space to download file\");\n\t\t\t\tdownloadFailed = true;\n\t\t\t} else {\n\t\t\t\t// If we are in a --dry-run situation - if not, actually perform the download\n\t\t\t\tif (!dryRun) {\n\t\t\t\t\t// Attempt to download the file as there is enough free space locally\n\t\t\t\t\tOneDriveApi downloadFileOneDriveApiInstance;\n\t\t\t\t\t\n\t\t\t\t\ttry {\t\n\t\t\t\t\t\t// Initialise API instance\n\t\t\t\t\t\tdownloadFileOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t\t\t\t\tdownloadFileOneDriveApiInstance.initialise();\n\t\t\t\t\t\t\n\t\t\t\t\t\t// OneDrive Business Shared Files - update the driveId where to get the file from\n\t\t\t\t\t\tif (isItemRemote(onedriveJSONItem)) {\n\t\t\t\t\t\t\tdownloadDriveId = onedriveJSONItem[\"remoteItem\"][\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Perform the download with any applicable set offset\n\t\t\t\t\t\tdownloadFileOneDriveApiInstance.downloadById(downloadDriveId, downloadItemId, newItemPath, jsonFileSize, onlineHash, resumeOffset);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\tdownloadFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\tdownloadFileOneDriveApiInstance = null;\n\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t\n\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"downloadFileOneDriveApiInstance.downloadById(downloadDriveId, downloadItemId, newItemPath, jsonFileSize, onlineHash, resumeOffset); generated a OneDriveException\", [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// HTTP request returned status code 403\n\t\t\t\t\t\tif ((exception.httpStatusCode == 403) && (appConfig.getValueBool(\"sync_business_shared_files\"))) {\n\t\t\t\t\t\t\t// We attempted to download a file, that was shared with us, but this was shared with us as read-only and no download permission\n\t\t\t\t\t\t\taddLogEntry(\"Unable to download this file as this was shared as read-only without download permission: \" ~ newItemPath);\n\t\t\t\t\t\t\tdownloadFailed = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Default operation if not a 403 error\n\t\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within downloadFileOneDriveApiInstance\n\t\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t\t// There was a file system error - display the error message\n\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, newItemPath, FsErrorSeverity.error);\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Download failed (local file system error): \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\tdownloadFailed = true;\n\t\t\t\t\t} catch (ErrnoException e) {\n\t\t\t\t\t\t// There was a file system error - display the error message\n\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, newItemPath, FsErrorSeverity.error);\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Download failed (local file system error): \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\tdownloadFailed = true;\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t\t// If we get to this point, something was downloaded .. does it match what we expected?\n\t\t\t\t\t// Does it still exist?\n\t\t\t\t\tif (exists(newItemPath)) {\n\t\t\t\t\t\t// When downloading some files from SharePoint, the OneDrive API reports one file size, \n\t\t\t\t\t\t// but the SharePoint HTTP Server sends a totally different byte count for the same file\n\t\t\t\t\t\t// we have implemented --disable-download-validation to disable these checks\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Regardless of --disable-download-validation we still need to set the file timestamp correctly\n\t\t\t\t\t\t// Get the mtime from the JSON data\n\t\t\t\t\t\tSysTime itemModifiedTime;\n\t\t\t\t\t\tstring lastModifiedTimestamp;\n\t\t\t\t\t\tif (isItemRemote(onedriveJSONItem)) {\n\t\t\t\t\t\t\t// remote file item\n\t\t\t\t\t\t\tlastModifiedTimestamp = strip(onedriveJSONItem[\"remoteItem\"][\"fileSystemInfo\"][\"lastModifiedDateTime\"].str);\n\t\t\t\t\t\t\t// is lastModifiedTimestamp valid?\n\t\t\t\t\t\t\tif (isValidUTCDateTime(lastModifiedTimestamp)) {\n\t\t\t\t\t\t\t\t// string is a valid timestamp\n\t\t\t\t\t\t\t\titemModifiedTime = SysTime.fromISOExtString(lastModifiedTimestamp);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// invalid timestamp from JSON file\n\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: Invalid timestamp provided by the Microsoft OneDrive API: \" ~ lastModifiedTimestamp);\n\t\t\t\t\t\t\t\t// Set mtime to Clock.currTime(UTC()) given that the time in the JSON should be a UTC timestamp\n\t\t\t\t\t\t\t\titemModifiedTime = Clock.currTime(UTC());\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// not a remote item\n\t\t\t\t\t\t\tlastModifiedTimestamp = strip(onedriveJSONItem[\"fileSystemInfo\"][\"lastModifiedDateTime\"].str);\n\t\t\t\t\t\t\t// is lastModifiedTimestamp valid?\n\t\t\t\t\t\t\tif (isValidUTCDateTime(lastModifiedTimestamp)) {\n\t\t\t\t\t\t\t\t// string is a valid timestamp\n\t\t\t\t\t\t\t\titemModifiedTime = SysTime.fromISOExtString(lastModifiedTimestamp);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// invalid timestamp from JSON file\n\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: Invalid timestamp provided by the Microsoft OneDrive API: \" ~ lastModifiedTimestamp);\n\t\t\t\t\t\t\t\t// Set mtime to Clock.currTime(UTC()) given that the time in the JSON should be a UTC timestamp\n\t\t\t\t\t\t\t\titemModifiedTime = Clock.currTime(UTC());\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Did the user configure --disable-download-validation ?\n\t\t\t\t\t\tif (!disableDownloadValidation) {\n\t\t\t\t\t\t\t// A 'file' was downloaded - does what we downloaded = reported jsonFileSize or if there is some sort of funky local disk compression going on\n\t\t\t\t\t\t\t// Does the file hash OneDrive reports match what we have locally?\n\t\t\t\t\t\t\tstring onlineFileHash;\n\t\t\t\t\t\t\tstring downloadedFileHash;\n\t\t\t\t\t\t\tlong downloadFileSize = getSize(newItemPath);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (!OneDriveFileXORHash.empty) {\n\t\t\t\t\t\t\t\tonlineFileHash = OneDriveFileXORHash;\n\t\t\t\t\t\t\t\t// Calculate the QuickXOHash for this file\n\t\t\t\t\t\t\t\tdownloadedFileHash = computeQuickXorHash(newItemPath);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tonlineFileHash = OneDriveFileSHA256Hash;\n\t\t\t\t\t\t\t\t// Fallback: Calculate the SHA256 Hash for this file\n\t\t\t\t\t\t\t\tdownloadedFileHash = computeSHA256Hash(newItemPath);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif ((downloadFileSize == jsonFileSize) && (downloadedFileHash == onlineFileHash)) {\n\t\t\t\t\t\t\t\t// Downloaded file matches size and hash\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Downloaded file matches reported size and reported file hash\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Set the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, newItemPath, itemModifiedTime);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// QuickXorHash in this client incorporates the file length into the final digest, so a size mismatch would be expected to also produce a hash mismatch. \n\t\t\t\t\t\t\t\t// However, QuickXorHash is not collision-resistant, so we treat the hash mismatch as the definitive integrity failure condition and log size mismatches \n\t\t\t\t\t\t\t\t// as advisory\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Downloaded file does not match size or hash .. which is it?\n\t\t\t\t\t\t\t\tbool downloadValueMismatch = false;\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Size difference between what was written to disk and what the API reported as the file size\n\t\t\t\t\t\t\t\tif (downloadFileSize != jsonFileSize) {\n\t\t\t\t\t\t\t\t\t// downloaded file size does not match\n\t\t\t\t\t\t\t\t\tdownloadValueMismatch = true;\n\t\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Actual file size on disk:   \" ~ to!string(downloadFileSize), [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"OneDrive API reported size: \" ~ to!string(jsonFileSize), [\"debug\"]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif ((verboseLogging)||(debugLogging)) {\n\t\t\t\t\t\t\t\t\t\t// verbose or debug message\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: Download validation failed (size mismatch): \" ~ newItemPath ~ \" | expected=\" ~ to!string(jsonFileSize) ~ \" | actual=\" ~ to!string(downloadFileSize), [\"verbose\"]);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// non-verbose message\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: File download size mismatch. Re-run with --verbose for additional diagnostic information to assist with troubleshooting.\");\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Hash difference between what was written to disk and then QuickXOR calculated and what the API reported as the file hash online\n\t\t\t\t\t\t\t\tif (downloadedFileHash != onlineFileHash) {\n\t\t\t\t\t\t\t\t\t// downloaded file hash does not match\n\t\t\t\t\t\t\t\t\tdownloadValueMismatch = true;\n\t\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Actual local file hash:     \" ~ downloadedFileHash, [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"OneDrive API reported hash: \" ~ onlineFileHash, [\"debug\"]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tif ((verboseLogging)||(debugLogging)) {\n\t\t\t\t\t\t\t\t\t\t// verbose or debug message\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Download validation failed (hash mismatch): \" ~ newItemPath ~ \" | expected=\" ~ onlineFileHash ~ \" | actual=\" ~ downloadedFileHash, [\"verbose\"]);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// non-verbose message\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: File download hash mismatch. Re-run with --verbose for additional diagnostic information to assist with troubleshooting.\");\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// .heic data loss check\n\t\t\t\t\t\t\t\t// - https://github.com/abraunegg/onedrive/issues/2471\n\t\t\t\t\t\t\t\t// - https://github.com/OneDrive/onedrive-api-docs/issues/1532\n\t\t\t\t\t\t\t\t// - https://github.com/OneDrive/onedrive-api-docs/issues/1723\n\t\t\t\t\t\t\t\tif (downloadValueMismatch && (toLower(extension(newItemPath)) == \".heic\")) {\n\t\t\t\t\t\t\t\t\t// Need to display a message to the user that they have experienced data loss\n\t\t\t\t\t\t\t\t\taddLogEntry(\"DATA-LOSS: File downloaded has experienced data loss due to a Microsoft OneDrive API bug. DO NOT DELETE THIS FILE ONLINE: \" ~ newItemPath, [\"info\", \"notify\"]);\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"           Please read https://github.com/OneDrive/onedrive-api-docs/issues/1723 for more details.\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Add some workaround messaging for SharePoint\n\t\t\t\t\t\t\t\tif (appConfig.accountType == \"documentLibrary\"){\n\t\t\t\t\t\t\t\t\t// It has been seen where SharePoint / OneDrive API reports one size via the JSON \n\t\t\t\t\t\t\t\t\t// but the content length and file size written to disk is totally different - example:\n\t\t\t\t\t\t\t\t\t// From JSON:         \"size\": 17133\n\t\t\t\t\t\t\t\t\t// From HTTPS Server: < Content-Length: 19340\n\t\t\t\t\t\t\t\t\t// with no logical reason for the difference, except for a 302 redirect before file download\n\t\t\t\t\t\t\t\t\taddLogEntry(\"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.\");\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// other account types\n\t\t\t\t\t\t\t\t\taddLogEntry(\"INFO: Potentially add --disable-download-validation to work around this issue but downloaded data integrity cannot be guaranteed.\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// If the computed hash does not equal provided online hash, consider this a failed download\n\t\t\t\t\t\t\t\tif (downloadedFileHash != onlineFileHash) {\n\t\t\t\t\t\t\t\t\t// We do not want this local file to remain on the local file system as it failed the integrity checks\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Removing local file \" ~ newItemPath ~ \" due to failed integrity checks\");\n\t\t\t\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\t\t\t\tsafeRemove(newItemPath);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Was this item previously in-sync with the local system?\n\t\t\t\t\t\t\t\t\t// We previously searched for the file in the DB, we need to use that record\n\t\t\t\t\t\t\t\t\tif (fileFoundInDB) {\n\t\t\t\t\t\t\t\t\t\t// Purge DB record so that the deleted local file does not cause an online deletion\n\t\t\t\t\t\t\t\t\t\t// In a --dry-run scenario, this is being done against a DB copy\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Removing DB record due to failed integrity checks\");\n\t\t\t\t\t\t\t\t\t\titemDB.deleteById(databaseItem.driveId, databaseItem.id);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Flag that the download failed\n\t\t\t\t\t\t\t\t\tdownloadFailed = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Download validation checks were disabled\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Downloaded file validation disabled due to --disable-download-validation\", [\"debug\"]);}\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"WARNING: Skipping download integrity check for: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Whilst the download integrity checks were disabled, we still have to set the correct timestamp on the file\n\t\t\t\t\t\t\t// Set the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, newItemPath, itemModifiedTime);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Azure Information Protection (AIP) protected files potentially have missing data and/or inconsistent data\n\t\t\t\t\t\t\tif (appConfig.accountType != \"personal\") {\n\t\t\t\t\t\t\t\t// AIP Protected Files cause issues here, as the online size & hash are not what has been downloaded\n\t\t\t\t\t\t\t\t// There is ZERO way to determine if this is an AIP protected file either from the JSON data\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Calculate the local file hash and get the local file size\n\t\t\t\t\t\t\t\tstring localFileHash = computeQuickXorHash(newItemPath);\n\t\t\t\t\t\t\t\tlong downloadFileSize = getSize(newItemPath);\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif ((OneDriveFileXORHash != localFileHash) && (jsonFileSize != downloadFileSize)) {\n\t\t\t\t\t\t\t\t\t// High potential to be an AIP protected file given the following scenario\n\t\t\t\t\t\t\t\t\t// Business | SharePoint Account Type (not a personal account)\n\t\t\t\t\t\t\t\t\t// --disable-download-validation is being used .. meaning the user has specifically configured this due the Microsoft SharePoint Enrichment Feature (bug)\n\t\t\t\t\t\t\t\t\t// The file downloaded but the XOR hash and file size locally is not as per the provided JSON - both are different\n\t\t\t\t\t\t\t\t\t//\n\t\t\t\t\t\t\t\t\t// Update the 'onedriveJSONItem' JSON data with the local values ..... \n\t\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\t\tstring 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);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(aipLogMessage, [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\" - Online XOR   : \" ~ to!string(OneDriveFileXORHash), [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\" - Online Size  : \" ~ to!string(jsonFileSize), [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\" - Local XOR    : \" ~ to!string(computeQuickXorHash(newItemPath)), [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\" - Local Size   : \" ~ to!string(getSize(newItemPath)), [\"debug\"]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Make the change in the JSON using local values\n\t\t\t\t\t\t\t\t\tonedriveJSONItem[\"file\"][\"hashes\"][\"quickXorHash\"] = localFileHash;\n\t\t\t\t\t\t\t\t\tonedriveJSONItem[\"size\"] = downloadFileSize;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\t// end of (!disableDownloadValidation)\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// File does not exist locally ... so the download failed\n\t\t\t\t\t\tif ((verboseLogging)||(debugLogging)) {\n\t\t\t\t\t\t\t// If we are doing verbose logging, \n\t\t\t\t\t\t\taddLogEntry(\"ERROR: Download failed (file not present after download): \" ~ newItemPath ~ \" | expectedSize=\" ~ to!string(jsonFileSize) ~ \" | resumeOffset=\" ~ to!string(resumeOffset), [\"verbose\"]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\taddLogEntry(\"ERROR: File failed to download. Re-run with --verbose for additional diagnostic information to assist with troubleshooting.\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Was this item previously in-sync with the local system?\n\t\t\t\t\t\t// We previously searched for the file in the DB, we need to use that record\n\t\t\t\t\t\tif (fileFoundInDB) {\n\t\t\t\t\t\t\t// Purge DB record so that the deleted local file does not cause an online deletion\n\t\t\t\t\t\t\t// In a --dry-run scenario, this is being done against a DB copy\n\t\t\t\t\t\t\taddLogEntry(\"Removing existing DB record due to failed file download.\");\n\t\t\t\t\t\t\titemDB.deleteById(databaseItem.driveId, databaseItem.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Flag that the download failed\n\t\t\t\t\t\tdownloadFailed = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// File should have been downloaded\n\t\t\tif (!downloadFailed) {\n\t\t\t\t// Download did not fail\n\t\t\t\taddLogEntry(\"Downloading file: \" ~ newItemPath ~ \" ... done\", fileTransferNotifications());\n\t\t\t\t\n\t\t\t\t// As no download failure, calculate transfer metrics in a consistent manner\n\t\t\t\tdisplayTransferMetrics(newItemPath, jsonFileSize, downloadStartTime, Clock.currTime());\n\t\t\t\t\n\t\t\t\t// Save this item into the database\n\t\t\t\tsaveItem(onedriveJSONItem);\n\t\t\t\t\n\t\t\t\t// If we are in a --dry-run situation - if we are, we need to track that we faked the download\n\t\t\t\tif (dryRun) {\n\t\t\t\t\t// track that we 'faked it'\n\t\t\t\t\tidsFaked ~= [downloadDriveId, downloadItemId];\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If, the initial download failed, but, during the 'Performing a last examination of the most recent online data within Microsoft OneDrive' Process\n\t\t\t\t// 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\n\t\t\t\tif (canFind(fileDownloadFailures, newItemPath)) {\n\t\t\t\t\t// Remove 'newItemPath' from 'fileDownloadFailures' as this is no longer a failed download\n\t\t\t\t\tfileDownloadFailures = fileDownloadFailures.filter!(item => item != newItemPath).array;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Did the user configure to save xattr data about this file?\n\t\t\t\tif (appConfig.getValueBool(\"write_xattr_data\")) {\n\t\t\t\t\twriteXattrData(newItemPath, onedriveJSONItem);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Output to the user that the file download failed\n\t\t\t\taddLogEntry(\"Downloading file: \" ~ newItemPath ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t\n\t\t\t\t// Add the path to a list of items that failed to download\n\t\t\t\tif (!canFind(fileDownloadFailures, newItemPath)) {\n\t\t\t\t\tfileDownloadFailures ~= newItemPath; // Add newItemPath if it's not already present\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Since the file download failed:\n\t\t\t\t// - The file should not exist locally\n\t\t\t\t// - The download identifiers should not exist in the local database\n\t\t\t\tif (!exists(newItemPath)) {\n\t\t\t\t\t// The local path does not exist\n\t\t\t\t\tif (itemDB.idInLocalDatabase(downloadDriveId, downloadItemId)) {\n\t\t\t\t\t\t// Since the path does not exist, but the driveId and itemId exists in the database, when we do the DB consistency check, we will think this file has been 'deleted'\n\t\t\t\t\t\t// The driveId and itemId online exists in our database - it needs to be removed so this does not occur\n\t\t\t\t\t\taddLogEntry(\"Removing existing DB record due to failed file download.\");\n\t\t\t\t\t\titemDB.deleteById(downloadDriveId, downloadItemId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\n\t// Write xattr data if configured to do so\n\tvoid writeXattrData(string filePath, JSONValue onedriveJSONItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// We can only set xattr values when not performing a --dry-run operation\n\t\tif (!dryRun) {\n\t\t\t// This function will write the following xattr attributes based on the JSON data received from Microsoft onedrive\n\t\t\t// - createdBy using the 'displayName' value\n\t\t\t// - lastModifiedBy using the 'displayName' value\n\t\t\tstring createdBy;\n\t\t\tstring lastModifiedBy;\n\t\t\t\n\t\t\t// Configure 'createdBy' from the JSON data\n\t\t\tif (hasCreatedByUserDisplayName(onedriveJSONItem)) {\n\t\t\t\tcreatedBy = onedriveJSONItem[\"createdBy\"][\"user\"][\"displayName\"].str;\n\t\t\t} else {\n\t\t\t\t// required data not in JSON data\n\t\t\t\tcreatedBy = \"Unknown\";\n\t\t\t}\n\t\t\t\n\t\t\t// Configure 'lastModifiedBy' from the JSON data\n\t\t\tif (hasLastModifiedByUserDisplayName(onedriveJSONItem)) {\n\t\t\t\tlastModifiedBy = onedriveJSONItem[\"lastModifiedBy\"][\"user\"][\"displayName\"].str;\n\t\t\t} else {\n\t\t\t\t// required data not in JSON data\n\t\t\t\tlastModifiedBy = \"Unknown\";\n\t\t\t}\n\t\t\t\n\t\t\t// Set the xattr values, file must exist to set these values\n\t\t\tif (exists(filePath)) {\n\t\t\t\tsetXAttr(filePath, \"user.onedrive.createdBy\", createdBy);\n\t\t\t\tsetXAttr(filePath, \"user.onedrive.lastModifiedBy\", lastModifiedBy);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\t\n\t}\n\t\n\t// Test if the given item is in-sync. Returns true if the given item corresponds to the local one\n\tbool isItemSynced(Item item, string path, string itemSource) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Due to this function, we need to keep the return <bool value>; code, so that this function operates as efficiently as possible.\n\t\t// It is pointless having the entire code run through and performing additional needless checks where it is not required\n\t\t// Whilst this means some extra code / duplication in this function, it cannot be helped\n\t\t\n\t\tif (!exists(path)) {\n\t\t\tif (debugLogging) {addLogEntry(\"Unable to determine the sync state of this file as it does not exist: \" ~ path, [\"debug\"]);}\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\n\t\t\treturn false;\n\t\t}\n\n\t\t// Combine common logic for readability and file check into a single block\n\t\tif (item.type == ItemType.file || ((item.type == ItemType.remote) && (item.remoteType == ItemType.file))) {\n\t\t\t// Can we actually read the local file?\n\t\t\tif (!readLocalFile(path)) {\n\t\t\t\t// Unable to read local file\n\t\t\t\taddLogEntry(\"Unable to determine the sync state of this file as it cannot be read (file permissions or file corruption): \" ~ path);\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t\n\t\t\t// Get time values\n\t\t\tSysTime localModifiedTime = timeLastModified(path).toUTC();\n\t\t\tSysTime itemModifiedTime = item.mtime;\n\t\t\t// Reduce time resolution to seconds before comparing\n\t\t\tlocalModifiedTime.fracSecs = Duration.zero;\n\t\t\titemModifiedTime.fracSecs = Duration.zero;\n\n\t\t\tif (localModifiedTime == itemModifiedTime) {\n\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\t// The file has a different timestamp ... is the hash the same meaning no file modification?\n\t\t\t\tif (verboseLogging) {\n\t\t\t\t\taddLogEntry(\"Local file time discrepancy detected: \" ~ path, [\"verbose\"]);\n\t\t\t\t\taddLogEntry(\"This local file has a different modified time \" ~ to!string(localModifiedTime) ~ \" (UTC) when compared to \" ~ itemSource ~ \" modified time \" ~ to!string(itemModifiedTime) ~ \" (UTC)\", [\"verbose\"]);\n\t\t\t\t}\n\n\t\t\t\t// The file has a different timestamp ... is the hash the same meaning no file modification?\n\t\t\t\t// Test the file hash as the date / time stamp is different\n\t\t\t\t// Generating a hash is computationally expensive - we only generate the hash if timestamp was different\n\t\t\t\tif (testFileHash(path, item)) {\n\t\t\t\t\t// The hash is the same .. so we need to fix-up the timestamp depending on where it is wrong\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Local item has the same hash value as the item online - correcting the applicable file timestamp\", [\"verbose\"]);}\n\t\t\t\t\t// Correction logic based on the configuration and the comparison of timestamps\n\t\t\t\t\tif (localModifiedTime > itemModifiedTime) {\n\t\t\t\t\t\t// Local file is newer timestamp wise, but has the same hash .. are we in a --download-only situation?\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"download_only\") && !dryRun) {\n\t\t\t\t\t\t\t// Not --download-only .. but are we in a --resync scenario?\n\t\t\t\t\t\t\tif (appConfig.getValueBool(\"resync\")) {\n\t\t\t\t\t\t\t\t// --resync was used\n\t\t\t\t\t\t\t\t// 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 ?\n\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The source of the incorrect timestamp was the local file - correcting timestamp locally due to --resync\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t// Fix the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, path, item.mtime);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// 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\n\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The source of the incorrect timestamp was OneDrive online - correcting timestamp online\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t// Attempt to update the online date time stamp\n\t\t\t\t\t\t\t\t// We need to use the correct driveId and itemId, especially if we are updating a OneDrive Business Shared File timestamp\n\t\t\t\t\t\t\t\tif (item.type == ItemType.file) {\n\t\t\t\t\t\t\t\t\t// Not a remote file\n\t\t\t\t\t\t\t\t\tuploadLastModifiedTime(item, item.driveId, item.id, localModifiedTime, item.eTag);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Remote file, remote values need to be used\n\t\t\t\t\t\t\t\t\tuploadLastModifiedTime(item, item.remoteDriveId, item.remoteId, localModifiedTime, item.eTag);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (!dryRun) {\n\t\t\t\t\t\t\t// --download-only is being used ... local file needs to be corrected ... but why is it newer - indexing application potentially changing the timestamp ?\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The source of the incorrect timestamp was the local file - correcting timestamp locally due to --download-only\", [\"verbose\"]);}\n\t\t\t\t\t\t\t// Fix the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, path, item.mtime);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (!dryRun) {\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The source of the incorrect timestamp was the local file - correcting timestamp locally\", [\"verbose\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Fix the timestamp, logging and error handling done within function\n\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, path, item.mtime);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\treturn false;\n\t\t\t\t} else {\n\t\t\t\t\t// The hash is different so the content of the file has to be different as to what is stored online\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The local file has a different hash when compared to \" ~ itemSource ~ \" file hash\", [\"verbose\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (item.type == ItemType.dir || ((item.type == ItemType.remote) && (item.remoteType == ItemType.dir))) {\n\t\t\t// item is a directory\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\treturn true;\n\t\t} else {\n\t\t\t// ItemType.unknown or ItemType.none\n\t\t\t// Logically, we might not want to sync these items, but a more nuanced approach may be needed based on application context\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\treturn true;\n\t\t}\n\t}\n\t\n\t// Get the /delta data using the provided details\n\tJSONValue getDeltaChangesByItemId(string selectedDriveId, string selectedItemId, string providedDeltaLink, OneDriveApi getDeltaQueryOneDriveApiInstance) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\n\t\t// Function variables\n\t\tJSONValue deltaChangesBundle;\n\t\t\n\t\t// Get the /delta data for this account | driveId | deltaLink combination\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(debugLogBreakType1, [\"debug\"]);\n\t\t\taddLogEntry(\"selectedDriveId:   \" ~ selectedDriveId, [\"debug\"]);\n\t\t\taddLogEntry(\"selectedItemId:    \" ~ selectedItemId, [\"debug\"]);\n\t\t\taddLogEntry(\"providedDeltaLink: \" ~ providedDeltaLink, [\"debug\"]);\n\t\t\taddLogEntry(debugLogBreakType1, [\"debug\"]);\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tdeltaChangesBundle = getDeltaQueryOneDriveApiInstance.getChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink);\n\t\t} catch (OneDriveException exception) {\n\t\t\t// caught an exception\n\t\t\tif (debugLogging) {addLogEntry(\"getDeltaQueryOneDriveApiInstance.getChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink) generated a OneDriveException\", [\"debug\"]);}\n\t\t\t\n\t\t\t// get the error message\n\t\t\tauto errorArray = splitLines(exception.msg);\n\t\t\t\n\t\t\t// Error handling operation if not 408,429,503,504 errors\n\t\t\t// - 408,429,503,504 errors are handled as a retry within getDeltaQueryOneDriveApiInstance\n\t\t\tif (exception.httpStatusCode == 410) {\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"WARNING: The OneDrive API responded with an error that indicates the locally stored deltaLink value is invalid\");\n\t\t\t\t// Essentially the 'providedDeltaLink' that we have stored is no longer available ... re-try without the stored deltaLink\n\t\t\t\taddLogEntry(\"WARNING: Retrying OneDrive API call without using the locally stored deltaLink value\");\n\t\t\t\t// Configure an empty deltaLink\n\t\t\t\tif (debugLogging) {addLogEntry(\"Delta link expired for 'getDeltaQueryOneDriveApiInstance.getChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink)', setting 'deltaLink = null'\", [\"debug\"]);}\n\t\t\t\tstring emptyDeltaLink = \"\";\n\t\t\t\t// retry with empty deltaLink\n\t\t\t\tdeltaChangesBundle = getDeltaQueryOneDriveApiInstance.getChangesByItemId(selectedDriveId, selectedItemId, emptyDeltaLink);\n\t\t\t} else {\n\t\t\t\t// Display what the error is\n\t\t\t\taddLogEntry(\"CODING TO DO: Hitting this failure error output after getting a httpStatusCode != 410 when the API responded the deltaLink was invalid\");\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\tdeltaChangesBundle = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return data\n\t\treturn deltaChangesBundle;\n\t}\n\t\n\t// If the JSON response is not correct JSON object, exit\n\tvoid invalidJSONResponseFromOneDriveAPI() {\n\t\taddLogEntry(\"ERROR: Query of the OneDrive API returned an invalid JSON response\");\n\t\t// Must force exit here, allow logging to be done\n\t\tforceExit();\n\t}\n\t\n\t// Handle an unhandled API error\n\tvoid defaultUnhandledHTTPErrorCode(OneDriveException exception) {\n\t\t// compute function name\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// display error\n\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t// Must force exit here, allow logging to be done\n\t\tforceExit();\n\t}\n\t\n\t// Display the pertinent details of the sync engine\n\tvoid displaySyncEngineDetails() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Display accountType, defaultDriveId, defaultRootId & remainingFreeSpace for verbose logging purposes\n\t\taddLogEntry(\"Application Version:   \" ~ appConfig.applicationVersion, [\"verbose\"]);\n\t\taddLogEntry(\"Account Type:          \" ~ appConfig.accountType, [\"verbose\"]);\n\t\taddLogEntry(\"Default Drive ID:      \" ~ appConfig.defaultDriveId, [\"verbose\"]);\n\t\taddLogEntry(\"Default Root ID:       \" ~ appConfig.defaultRootId, [\"verbose\"]);\n\t\taddLogEntry(\"Microsoft Data Centre: \" ~ microsoftDataCentre, [\"verbose\"]);\n\t\n\t\t// Fetch the details from cachedOnlineDriveData\n\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\tcachedOnlineDriveData = getDriveDetails(appConfig.defaultDriveId);\n\t\n\t\t// What do we display here for space remaining\n\t\tif (cachedOnlineDriveData.quotaRemaining > 0) {\n\t\t\t// Display the actual value\n\t\t\taddLogEntry(\"Remaining Free Space:  \" ~ to!string(byteToGibiByte(cachedOnlineDriveData.quotaRemaining)) ~ \" GB (\" ~ to!string(cachedOnlineDriveData.quotaRemaining) ~ \" bytes)\", [\"verbose\"]);\n\t\t} else {\n\t\t\t// zero or non-zero value or restricted\n\t\t\tif (!cachedOnlineDriveData.quotaRestricted){\n\t\t\t\taddLogEntry(\"Remaining Free Space:  0 KB\", [\"verbose\"]);\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"Remaining Free Space:  Not Available\", [\"verbose\"]);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Query itemdb.computePath() and catch potential assert when DB consistency issue occurs\n\t// This function returns what that local physical path should be on the local disk\n\tstring computeItemPath(string thisDriveId, string thisItemId) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// static declare this for this function\n\t\tstatic import core.exception;\n\t\tstring calculatedPath;\n\t\t\n\t\t// Issue #3336 - Convert thisDriveId to lowercase before any test\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\tthisDriveId = transformToLowerCase(thisDriveId);\n\t\t}\n\t\t\n\t\t// What driveID and itemID we trying to calculate the path for\n\t\tif (debugLogging) {\n\t\t\tstring initialComputeLogMessage = format(\"Attempting to calculate local filesystem path for '%s' and '%s'\", thisDriveId, thisItemId);\n\t\t\taddLogEntry(initialComputeLogMessage, [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Perform the original calculation of the path using the values provided\n\t\ttry {\n\t\t\t// The 'itemDB.computePath' will calculate the full path for the combination of provided driveId and itemId values.\n\t\t\t// This function traverses the parent chain of a given item (e.g., folder or file) using stored parent-child relationships \n\t\t\t// in the database, reconstructing the correct path from the item's root to itself.\n\t\t\tcalculatedPath = itemDB.computePath(thisDriveId, thisItemId);\n\t\t\tif (debugLogging) {addLogEntry(\"Calculated local path via itemDB.computePath() = \" ~ to!string(calculatedPath), [\"debug\"]);}\n\t\t} catch (core.exception.AssertError) {\n\t\t\t// broken tree in the database, we cant compute the path for this item id, exit\n\t\t\taddLogEntry(\"ERROR: A database consistency issue has been caught. A --resync is needed to rebuild the database.\");\n\t\t\t// Must force exit here, allow logging to be done\n\t\t\tforceExit();\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return calculated path as string\n\t\treturn calculatedPath;\n\t}\n\t\n\t// Try and compute the file hash for the given item\n\tbool testFileHash(string path, Item item) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Due to this function, we need to keep the return <bool value>; code, so that this function operates as efficiently as possible.\n\t\t// It is pointless having the entire code run through and performing additional needless checks where it is not required\n\t\t// Whilst this means some extra code / duplication in this function, it cannot be helped\n\t\t\n\t\t// Generate QuickXORHash first before attempting to generate any other type of hash\n\t\tif (item.quickXorHash) {\n\t\t\tif (item.quickXorHash == computeQuickXorHash(path)) {\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (item.sha256Hash) {\n\t\t\tif (item.sha256Hash == computeSHA256Hash(path)) {\n\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\treturn false;\n\t}\n\t\n\t// Process items that need to be removed from the local filesystem as they were removed online\n\tvoid processDeleteItems() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Has the user configured to use the 'Recycle Bin' locally, for any files that are deleted online?\n\t\tif (!appConfig.getValueBool(\"use_recycle_bin\")) {\n\t\t\n\t\t\tif (debugLogging) {addLogEntry(\"Performing filesystem deletion, using reverse order of items to delete\", [\"debug\"]);}\n\t\t\n\t\t\tforeach_reverse (i; idsToDelete) {\n\t\t\t\tItem item;\n\t\t\t\tstring path;\n\t\t\t\tif (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db\n\t\t\t\t// Compute this item path\n\t\t\t\tpath = computeItemPath(i[0], i[1]);\n\t\t\t\t\n\t\t\t\t// Log the action if the path exists .. it may of already been removed and this is a legacy array item\n\t\t\t\tif (exists(path)) {\n\t\t\t\t\tif (item.type == ItemType.file) {\n\t\t\t\t\t\taddLogEntry(\"Trying to delete local file: \" ~ path);\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"Trying to delete local directory: \" ~ path);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy\n\t\t\t\titemDB.deleteById(item.driveId, item.id);\n\t\t\t\tif (item.remoteDriveId != null) {\n\t\t\t\t\t// delete the linked remote folder\n\t\t\t\t\titemDB.deleteById(item.remoteDriveId, item.remoteId);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Add to pathFakeDeletedArray\n\t\t\t\t// We dont want to try and upload this item again, so we need to track this objects removal\n\t\t\t\tif (dryRun) {\n\t\t\t\t\t// We need to add './' here so that it can be correctly searched to ensure it is not uploaded\n\t\t\t\t\tstring pathToAdd = \"./\" ~ path;\n\t\t\t\t\tpathFakeDeletedArray ~= pathToAdd;\n\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\tbool needsRemoval = false;\n\t\t\t\tif (exists(path)) {\n\t\t\t\t\t// path exists on the local system\t\n\t\t\t\t\t// make sure that the path refers to the correct item\n\t\t\t\t\tItem pathItem;\n\t\t\t\t\tif (itemDB.selectByPath(path, item.driveId, pathItem)) {\n\t\t\t\t\t\tif (pathItem.id == item.id) {\n\t\t\t\t\t\t\tneedsRemoval = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\taddLogEntry(\"Skipping local path removal due to 'id' difference!\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// item has disappeared completely\n\t\t\t\t\t\tneedsRemoval = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (needsRemoval) {\n\t\t\t\t\t// Log the action\n\t\t\t\t\tif (item.type == ItemType.file) {\n\t\t\t\t\t\taddLogEntry(\"Deleting local file: \" ~ path, fileTransferNotifications());\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"Deleting local directory: \" ~ path, fileTransferNotifications());\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Perform the action\n\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\tif (isFile(path)) {\n\t\t\t\t\t\t\tsafeRemove(path);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t// Remove any children of this path if they still exist\n\t\t\t\t\t\t\t\t// Resolve 'Directory not empty' error when deleting local files\n\t\t\t\t\t\t\t\tforeach (DirEntry child; dirEntries(path, SpanMode.depth, false)) {\n\t\t\t\t\t\t\t\t\tattrIsDir(child.linkAttributes) ? rmdir(child.name) : safeRemove(child.name);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Remove the path now that it is empty of children\n\t\t\t\t\t\t\t\trmdirRecurse(path);\n\t\t\t\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t\t\t\t// display the error message\n\t\t\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, path);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t} else {\n\t\t\n\t\t\tif (debugLogging) {addLogEntry(\"Moving online deleted files to configured local Recycle Bin\", [\"debug\"]);}\n\t\t\t\n\t\t\t// Process in normal order, so that the parent, if a folder, gets moved 'first' mirroring how files / folders are deleted in GNOME and KDE\n\t\t\tforeach (i; idsToDelete) {\n\t\t\t\tItem item;\n\t\t\t\tstring path;\n\t\t\t\tif (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db\n\t\t\t\t// Compute this item path\n\t\t\t\tpath = computeItemPath(i[0], i[1]);\n\t\t\t\t\n\t\t\t\t// Log the action if the path exists .. it may of already been removed and this is a legacy array item\n\t\t\t\tif (exists(path)) {\n\t\t\t\t\tif (item.type == ItemType.file) {\n\t\t\t\t\t\taddLogEntry(\"Trying to move this local file to the configured 'Recycle Bin': \" ~ path);\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"Trying to move this local directory to the configured 'Recycle Bin': \" ~ path);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy\n\t\t\t\titemDB.deleteById(item.driveId, item.id);\n\t\t\t\tif (item.remoteDriveId != null) {\n\t\t\t\t\t// delete the linked remote folder\n\t\t\t\t\titemDB.deleteById(item.remoteDriveId, item.remoteId);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Add to pathFakeDeletedArray\n\t\t\t\t// We dont want to try and upload this item again, so we need to track this objects removal\n\t\t\t\tif (dryRun) {\n\t\t\t\t\t// We need to add './' here so that it can be correctly searched to ensure it is not uploaded\n\t\t\t\t\tstring pathToAdd = \"./\" ~ path;\n\t\t\t\t\tpathFakeDeletedArray ~= pathToAdd;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Local path removal\n\t\t\t\tbool needsRemoval = false;\n\t\t\t\tif (exists(path)) {\n\t\t\t\t\t// path exists on the local system\t\n\t\t\t\t\t// make sure that the path refers to the correct item\n\t\t\t\t\tItem pathItem;\n\t\t\t\t\tif (itemDB.selectByPath(path, item.driveId, pathItem)) {\n\t\t\t\t\t\tif (pathItem.id == item.id) {\n\t\t\t\t\t\t\tneedsRemoval = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\taddLogEntry(\"Skipping local path removal due to 'id' difference!\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// item has disappeared completely\n\t\t\t\t\t\tneedsRemoval = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (needsRemoval) {\n\t\t\t\t\t// Log the action\n\t\t\t\t\tif (item.type == ItemType.file) {\n\t\t\t\t\t\taddLogEntry(\"Moving this local file to the configured 'Recycle Bin': \" ~ path, fileTransferNotifications());\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"Moving this local directory to the configured 'Recycle Bin': \" ~ path, fileTransferNotifications());\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Perform the action\n\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t// Move the 'path' to the configured recycle bin\n\t\t\t\t\t\tmovePathToRecycleBin(path);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (!dryRun) {\n\t\t\t// Cleanup array memory\n\t\t\tidsToDelete = [];\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\n\t// Move to the 'Recycle Bin' rather than a hard delete locally of the online deleted item\t\n\tvoid movePathToRecycleBin(string path) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// This is a 2 step process\n\t\t// 1. Move the file\n\t\t//    - If the destination 'name' already exists, the file being moved to the 'Recycle Bin' needs to have a number added to it.\n\t\t// 2. Create the metadata about where the file came from\n\t\t//    - This is in a specific format:\n\t\t//    \t\t[Trash Info]\n\t\t//    \t\tPath=/original/absolute/path/to/the/file/or/folder\n\t\t//    \t\tDeletionDate=YYYY-MM-DDTHH:MM:SS\n\t\t\n\t\t// Calculate all the initial paths required\n\t\tstring computedFullLocalPath = absolutePath(path);\n\t\tstring fileNameOnly = baseName(path);\n\t\tstring computedRecycleBinFilePath = appConfig.recycleBinFilePath ~ fileNameOnly;\n\t\tstring computedRecycleBinInfoPath = appConfig.recycleBinInfoPath ~ fileNameOnly ~ \".trashinfo\";\n\t\tbool isPathFile = isFile(computedFullLocalPath);\n\t\t\n\t\t// The 'destination' needs to be unique, but if there is a 'collision' the RecycleBin paths need to be updated to be:\n\t\t// - file1.data (1)\n\t\t// - file1.data (1).trashinfo\n\t\tif (exists(computedRecycleBinFilePath)) {\n\t\t\t// There is an existing file with the same name already in the 'Recycle Bin'\n\t\t\t// - Testing has show that this counter MUST start at 2 to be compatible with FreeDesktop.org Trash Specification ....\n\t\t\tint n = 2;\n\t\t\t\n\t\t\t// We need to split this out\n\t\t\tstring nameOnly = stripExtension(fileNameOnly); // \"file1\"\n\t\t\tstring extension = extension(fileNameOnly);     // \".data\"\n\t\t\t\n\t\t\t// We need to test for this: nameOnly.n.extension\n\t\t\twhile (exists(format(appConfig.recycleBinFilePath ~ nameOnly ~ \".%d.\" ~ extension, n))) {\n\t\t\t\tn++;\n\t\t\t}\n\t\t\t\n\t\t\t// Generate newFileNameOnly\n\t\t\tstring newFileNameOnly = format(nameOnly ~ \".%d.\" ~ extension, n);\n\t\t\t\n\t\t\t// UPDATE:\n\t\t\t// - computedRecycleBinFilePath\n\t\t\t// - computedRecycleBinInfoPath\n\t\t\tcomputedRecycleBinFilePath = appConfig.recycleBinFilePath ~ newFileNameOnly;\n\t\t\tcomputedRecycleBinInfoPath = appConfig.recycleBinInfoPath ~ newFileNameOnly ~ \".trashinfo\";\n\t\t}\n\t\t\n\t\t// Move the file to the 'Recycle Bin' path computedRecycleBinFilePath\n\t\t// - DMD has no 'move' specifically, it uses 'rename' to achieve this\n\t\t//   https://forum.dlang.org/thread/kwnwrlqtjehldckyfmau@forum.dlang.org\n\t\t// Use rename() as Linux is POSIX compliant, we have an atomic operation where at no point in time the 'to' is missing.\n\t\ttry {\n\t\t\trename(computedFullLocalPath, computedRecycleBinFilePath);\n\t\t} catch (Exception e) {\n\t\t\t// Handle exceptions, e.g., log error\n\t\t\tif (isPathFile) {\n\t\t\t\taddLogEntry(\"Move of local file failed for \" ~ to!string(path) ~ \": \" ~ e.msg, [\"error\"]);\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"Move of local directory failed for \" ~ to!string(path) ~ \": \" ~ e.msg, [\"error\"]);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Generate the 'Recycle Bin' metadata file using computedRecycleBinInfoPath\n\t\tauto now = Clock.currTime().toLocalTime();\n\t\tstring deletionDate = format(\"%04d-%02d-%02dT%02d:%02d:%02d\",now.year, now.month, now.day, now.hour, now.minute, now.second);\n\t\t\n\t\t// Format the content of the .trashinfo file\n\t\tstring content = format(\"[Trash Info]\\nPath=%s\\nDeletionDate=%s\\n\", computedFullLocalPath, deletionDate);\n\t\t// Write the metadata file\n\t\t\n\t\ttry {\n\t\t\tstd.file.write(computedRecycleBinInfoPath, content);\n\t\t} catch (Exception e) {\n\t\t\t// Handle exceptions, e.g., log error\n\t\t\taddLogEntry(\"Writing of .trashinfo metadata file failed for \" ~ computedRecycleBinInfoPath ~ \": \" ~ e.msg, [\"error\"]);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// List items that were deleted online, but, due to --download-only being used, will not be deleted locally\n\tvoid listDeletedItems() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// For each id in the idsToDelete array\n\t\tforeach_reverse (i; idsToDelete) {\n\t\t\tItem item;\n\t\t\tstring path;\n\t\t\tif (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db\n\t\t\t// Compute this item path\n\t\t\tpath = computeItemPath(i[0], i[1]);\n\t\t\t\n\t\t\t// Log the action if the path exists .. it may of already been removed and this is a legacy array item\n\t\t\tif (exists(path)) {\n\t\t\t\tif (item.type == ItemType.file) {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping local deletion for file \" ~ path, [\"verbose\"]);}\n\t\t\t\t} else {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping local deletion for directory \" ~ path, [\"verbose\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Update the timestamp of an object online\n\tvoid uploadLastModifiedTime(Item originItem, string driveId, string id, SysTime mtime, string eTag) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\tstring itemModifiedTime;\n\t\titemModifiedTime = mtime.toISOExtString();\n\t\tJSONValue data = [\n\t\t\t\"fileSystemInfo\": JSONValue([\n\t\t\t\t\"lastModifiedDateTime\": itemModifiedTime\n\t\t\t])\n\t\t];\n\t\t\n\t\t// What eTag value do we use?\n\t\tstring eTagValue;\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Nullify the eTag to avoid 412 errors as much as possible\n\t\t\teTagValue = null;\n\t\t} else {\n\t\t\teTagValue = eTag;\n\t\t}\n\t\t\n\t\tJSONValue response;\n\t\tOneDriveApi uploadLastModifiedTimeApiInstance;\n\t\t\n\t\t// Try and update the online last modified time\n\t\ttry {\n\t\t\t// Create a new OneDrive API instance\n\t\t\tuploadLastModifiedTimeApiInstance = new OneDriveApi(appConfig);\n\t\t\tuploadLastModifiedTimeApiInstance.initialise();\n\t\t\t// Use this instance\n\t\t\tresponse = uploadLastModifiedTimeApiInstance.updateById(driveId, id, data, eTagValue);\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tuploadLastModifiedTimeApiInstance.releaseCurlEngine();\n\t\t\tuploadLastModifiedTimeApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t\t// Do we actually save the response?\n\t\t\t// 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\n\t\t\t//  Update 'originItem.mtime' with the correct timestamp\n\t\t\t//  Update 'originItem.size' with the correct size from the response\n\t\t\t//  Update 'originItem.eTag' with the correct eTag from the response\n\t\t\t//  Update 'originItem.cTag' with the correct cTag from the response\n\t\t\t//  Update 'originItem.quickXorHash' with the correct quickXorHash from the response\n\t\t\t// Everything else should remain the same .. and then save this DB record to the DB ..\n\t\t\t// 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?\n\t\t\tif ((originItem.type != ItemType.remote) && (originItem.remoteType != ItemType.file)) {\n\t\t\t\tif (response.type() == JSONType.object) {\n\t\t\t\t\t// Save the response JSON\n\t\t\t\t\tsaveItem(response);\n\t\t\t\t} else {\n\t\t\t\t\t// Log why we are not saving \n\t\t\t\t\tif (debugLogging) {addLogEntry(\"uploadLastModifiedTime: updateById returned no JSON payload (likely HTTP 204); skipping saveItem()\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t} \n\t\t} catch (OneDriveException exception) {\n\t\t\t// Handle a 409 - ETag does not match current item's value\n\t\t\t// Handle a 412 - A precondition provided in the request (such as an if-match header) does not match the resource's current state.\n\t\t\tif ((exception.httpStatusCode == 409) || (exception.httpStatusCode == 412)) {\n\t\t\t\t// Handle the 409\n\t\t\t\tif (exception.httpStatusCode == 409) {\n\t\t\t\t\t// OneDrive threw a 412 error\n\t\t\t\t\tif (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\"]);}\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"File Metadata Update Failed - OneDrive eTag / cTag match issue\", [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"Retrying Function: \" ~ thisFunctionName, [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Handle the 412\n\t\t\t\tif (exception.httpStatusCode == 412) {\n\t\t\t\t\t// OneDrive threw a 412 error\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"OneDrive returned a 'HTTP 412 - Precondition Failed' when attempting file time stamp update - gracefully handling error\", [\"verbose\"]);}\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"File Metadata Update Failed - OneDrive eTag / cTag match issue\", [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"Retrying Function: \" ~ thisFunctionName, [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Retry without eTag\n\t\t\t\tuploadLastModifiedTime(originItem, driveId, id, mtime, null);\n\t\t\t} else {\n\t\t\t\t// Any other error that should be handled\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t// Display what the error is\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tuploadLastModifiedTimeApiInstance.releaseCurlEngine();\n\t\t\tuploadLastModifiedTimeApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// 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\n\tvoid performDatabaseConsistencyAndIntegrityCheck() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Log what we are doing\n\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\taddProcessingLogHeaderEntry(\"Performing a database consistency and integrity check on locally stored data\", appConfig.verbosityCount);\n\t\t}\n\t\t\n\t\t// What driveIDsArray do we use? If we are doing a --single-directory we need to use just the drive id associated with that operation\n\t\tstring[] consistencyCheckDriveIdsArray;\n\t\tif (singleDirectoryScope) {\n\t\t\tconsistencyCheckDriveIdsArray ~= singleDirectoryScopeDriveId;\n\t\t} else {\n\t\t\t// Query the DB for all unique DriveID's\n\t\t\tconsistencyCheckDriveIdsArray = itemDB.selectDistinctDriveIds();\n\t\t}\n\t\t\n\t\t// Create a new DB blank item\n\t\tItem item;\n\t\t// Use the array we populate, rather than selecting all distinct driveId's from the database\n\t\tforeach (driveId; consistencyCheckDriveIdsArray) {\n\t\t\t// Make the logging more accurate - we cant update driveId as this then breaks the below queries\n\t\t\tif (verboseLogging) {addLogEntry(\"Processing DB entries for this Drive ID: \" ~ driveId, [\"verbose\"]);}\n\t\t\t\n\t\t\t// Initialise the array \n\t\t\tItem[] driveItems = [];\n\t\t\t\n\t\t\t// Freshen the cached quota details for this driveID\n\t\t\taddOrUpdateOneDriveOnlineDetails(driveId);\n\n\t\t\t// What OneDrive API query do we use?\n\t\t\t// - Are we running against a National Cloud Deployments that does not support /delta ?\n\t\t\t//   National Cloud Deployments do not support /delta as a query\n\t\t\t//   https://docs.microsoft.com/en-us/graph/deployments#supported-features\n\t\t\t//\n\t\t\t// - Are we performing a --single-directory sync, which will exclude many items online, focusing in on a specific online directory\n\t\t\t// - Are we performing a --download-only --cleanup-local-files action?\n\t\t\t// - Are we scanning a Shared Folder\n\t\t\t//\n\t\t\t// If we did, we self generated a /delta response, thus need to now process elements that are still flagged as out-of-sync\n\t\t\tif ((singleDirectoryScope) || (nationalCloudDeployment) || (cleanupLocalFiles) || sharedFolderDeltaGeneration) {\n\t\t\t\t// 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\n\t\t\t\t// Normally, this is done at the end of processing all /delta queries, however when using --single-directory or a National Cloud Deployments is configured\n\t\t\t\t// We cant use /delta to query the OneDrive API as National Cloud Deployments dont support /delta\n\t\t\t\t// https://docs.microsoft.com/en-us/graph/deployments#supported-features\n\t\t\t\t// 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\n\t\t\t\t// 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\n\n\t\t\t\t// For each unique OneDrive driveID we know about\n\t\t\t\tItem[] outOfSyncItems = itemDB.selectOutOfSyncItems(driveId);\n\t\t\t\tforeach (outOfSyncItem; outOfSyncItems) {\n\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t// clean up idsToDelete\n\t\t\t\t\t\tidsToDelete.length = 0;\n\t\t\t\t\t\tassumeSafeAppend(idsToDelete);\n\t\t\t\t\t\t// flag to delete local file as it now is no longer in sync with OneDrive\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"Flagging to delete local item as it now is no longer in sync with OneDrive\", [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"outOfSyncItem: \" ~ to!string(outOfSyncItem), [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Use the configured values - add the driveId, itemId and parentId values to the array\n\t\t\t\t\t\tidsToDelete ~= [outOfSyncItem.driveId, outOfSyncItem.id, outOfSyncItem.parentId];\n\t\t\t\t\t\t// delete items in idsToDelete\n\t\t\t\t\t\tif (idsToDelete.length > 0) processDeleteItems();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Clear array\n\t\t\t\toutOfSyncItems = [];\n\t\t\t\t\t\t\n\t\t\t\t// Fetch database items associated with this path\n\t\t\t\tif (singleDirectoryScope) {\n\t\t\t\t\t// Use the --single-directory items we previously configured\n\t\t\t\t\t// - query database for children objects using those items\n\t\t\t\t\tdriveItems = getChildren(singleDirectoryScopeDriveId, singleDirectoryScopeItemId);\n\t\t\t\t} else {\n\t\t\t\t\t// Check everything associated with each driveId we know about\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Selecting DB items via itemDB.selectByDriveId(driveId)\", [\"debug\"]);}\n\t\t\t\t\t// Query database\n\t\t\t\t\tdriveItems = itemDB.selectByDriveId(driveId);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Log DB items to process\n\t\t\t\tif (debugLogging) {addLogEntry(\"Database items to process for this driveId: \" ~ to!string(driveItems.count), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Process each database item associated with the driveId\n\t\t\t\tforeach(dbItem; driveItems) {\n\t\t\t\t\t// Does it still exist on disk in the location the DB thinks it is\n\t\t\t\t\tcheckDatabaseItemForConsistency(dbItem);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Check everything associated with each driveId we know about\n\t\t\t\tif (debugLogging) {addLogEntry(\"Selecting DB items via itemDB.selectByDriveId(driveId)\", [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Query database\n\t\t\t\tdriveItems = itemDB.selectByDriveId(driveId);\n\t\t\t\tif (debugLogging) {addLogEntry(\"Database items to process for this driveId: \" ~ to!string(driveItems.count), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Process each database item associated with the driveId\n\t\t\t\tforeach(dbItem; driveItems) {\n\t\t\t\t\t// Does it still exist on disk in the location the DB thinks it is\n\t\t\t\t\tcheckDatabaseItemForConsistency(dbItem);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Clear the array\n\t\t\tdriveItems = [];\n\t\t}\n\n\t\t// Close out the '....' being printed to the console\n\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\tcompleteProcessingDots();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Are we doing a --download-only sync?\n\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\t\n\t\t\t// Do we have any known items, where they have been deleted locally, that now need to be deleted online?\n\t\t\tif (databaseItemsToDeleteOnline.length > 0) {\n\t\t\t\t// There are items to delete online\n\t\t\t\taddLogEntry(\"Deleted local items to delete on Microsoft OneDrive: \" ~ to!string(databaseItemsToDeleteOnline.length));\n\t\t\t\tforeach(localItemToDeleteOnline; databaseItemsToDeleteOnline) {\n\t\t\t\t\t// Upload to OneDrive the instruction to delete this item. This will handle the 'noRemoteDelete' flag if set\n\t\t\t\t\tuploadDeletedItem(localItemToDeleteOnline.dbItem, localItemToDeleteOnline.localFilePath);\n\t\t\t\t}\n\t\t\t\t// Cleanup array memory\n\t\t\t\tdatabaseItemsToDeleteOnline = [];\n\t\t\t}\n\t\t\t\n\t\t\t// Do we have any known items, where the content has changed locally, that needs to be uploaded?\n\t\t\tif (databaseItemsWhereContentHasChanged.length > 0) {\n\t\t\t\t// There are changed local files that were in the DB to upload\n\t\t\t\taddLogEntry(\"Changed local items to upload to Microsoft OneDrive: \" ~ to!string(databaseItemsWhereContentHasChanged.length));\n\t\t\t\tprocessChangedLocalItemsToUpload();\n\t\t\t\t// Cleanup array memory\n\t\t\t\tdatabaseItemsWhereContentHasChanged = [];\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Check this Database Item for its consistency on disk\n\tvoid checkDatabaseItemForConsistency(Item dbItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Due to this function, we need to keep the return <bool value>; code, so that this function operates as efficiently as possible.\n\t\t// It is pointless having the entire code run through and performing additional needless checks where it is not required\n\t\t// Whilst this means some extra code / duplication in this function, it cannot be helped\n\t\t\t\n\t\t// What is the local path item\n\t\tstring localFilePath;\n\t\t// Do we want to onward process this item?\n\t\tbool unwanted = false;\n\t\t\n\t\t// Remote directory items we can 'skip'\n\t\tif ((dbItem.type == ItemType.remote) && (dbItem.remoteType == ItemType.dir)) {\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\t// return .. nothing to check here, no logging needed\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Compute this dbItem path early as we we use this path often\n\t\tlocalFilePath = buildNormalizedPath(computeItemPath(dbItem.driveId, dbItem.id));\n\t\t\n\t\t// To improve logging output for this function, what is the 'logical path'?\n\t\tstring logOutputPath;\n\t\tif (localFilePath == \".\") {\n\t\t\t// get the configured sync_dir\n\t\t\tlogOutputPath = buildNormalizedPath(appConfig.getValueString(\"sync_dir\"));\n\t\t} else {\n\t\t\t// Use the path that was computed\n\t\t\tlogOutputPath = localFilePath;\n\t\t}\n\t\t\n\t\t// Log what we are doing\n\t\tif (verboseLogging) {addLogEntry(\"Processing: \" ~ logOutputPath, [\"verbose\"]);}\n\t\t// Add a processing '.'\n\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\taddProcessingDotEntry();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Debug logging of paths being checked\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Database item being checked: \" ~ to!string(dbItem), [\"debug\"]);\n\t\t\taddLogEntry(\"Local Path being checked:    \" ~ localFilePath, [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Determine which action to take\n\t\tfinal switch (dbItem.type) {\n\t\tcase ItemType.file:\n\t\t\t// Logging output result is handled by checkFileDatabaseItemForConsistency\n\t\t\tcheckFileDatabaseItemForConsistency(dbItem, localFilePath);\n\t\t\tgoto functionCompletion;\n\t\t\t\n\t\tcase ItemType.dir, ItemType.root:\n\t\t\t// Logging output result is handled by checkDirectoryDatabaseItemForConsistency\n\t\t\tcheckDirectoryDatabaseItemForConsistency(dbItem, localFilePath);\n\t\t\tgoto functionCompletion;\n\t\t\t\n\t\tcase ItemType.remote:\n\t\t\t// DB items that match: dbItem.remoteType == ItemType.dir - these should have been skipped above\n\t\t\t// This means that anything that hits here should be: dbItem.remoteType == ItemType.file\n\t\t\tcheckFileDatabaseItemForConsistency(dbItem, localFilePath);\n\t\t\tgoto functionCompletion;\n\t\t\t\n\t\tcase ItemType.unknown:\n\t\tcase ItemType.none:\n\t\t\t// Unknown type - we dont action these items\n\t\t\tgoto functionCompletion;\n\t\t}\n\t\t\n\t\t// 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\n\t\tfunctionCompletion:\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t}\n\t\n\t// Perform the database consistency check on this file item\n\tvoid checkFileDatabaseItemForConsistency(Item dbItem, string localFilePath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// What is the source of this item data?\n\t\tstring itemSource = \"database\";\n\n\t\t// Does this item|file still exist on disk?\n\t\tif (exists(localFilePath)) {\n\t\t\t// Path exists locally, is this path a file?\n\t\t\tif (isFile(localFilePath)) {\n\t\t\t\t// Can we actually read the local file?\n\t\t\t\tif (readLocalFile(localFilePath)){\n\t\t\t\t\t// File is readable\n\t\t\t\t\tSysTime localModifiedTime = timeLastModified(localFilePath).toUTC();\n\t\t\t\t\tSysTime itemModifiedTime = dbItem.mtime;\n\t\t\t\t\t// Reduce time resolution to seconds before comparing\n\t\t\t\t\titemModifiedTime.fracSecs = Duration.zero;\n\t\t\t\t\tlocalModifiedTime.fracSecs = Duration.zero;\n\t\t\t\t\t\n\t\t\t\t\tif (localModifiedTime != itemModifiedTime) {\n\t\t\t\t\t\t// The modified dates are different\n\t\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"Local file time discrepancy detected: \" ~ localFilePath, [\"verbose\"]);\n\t\t\t\t\t\t\taddLogEntry(\"This local file has a different modified time \" ~ to!string(localModifiedTime) ~ \" (UTC) when compared to \" ~ itemSource ~ \" modified time \" ~ to!string(itemModifiedTime) ~ \" (UTC)\", [\"verbose\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Test the file hash\n\t\t\t\t\t\tif (!testFileHash(localFilePath, dbItem)) {\n\t\t\t\t\t\t\t// Is the local file 'newer' or 'older' (ie was an old file 'restored locally' by a different backup / replacement process?)\n\t\t\t\t\t\t\tif (localModifiedTime >= itemModifiedTime) {\n\t\t\t\t\t\t\t\t// Local file is newer\n\t\t\t\t\t\t\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The file content has changed locally and has a newer timestamp, thus needs to be uploaded to OneDrive\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t// 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\n\t\t\t\t\t\t\t\t\tdatabaseItemsWhereContentHasChanged ~= [dbItem.driveId, dbItem.id, localFilePath];\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Local file is older - data recovery process? something else?\n\t\t\t\t\t\t\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\t\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t\t\t\t// 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\n\t\t\t\t\t\t\t\t\tdatabaseItemsWhereContentHasChanged ~= [dbItem.driveId, dbItem.id, localFilePath];\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// The file contents have not changed, but the modified timestamp has\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The last modified timestamp has changed however the file content has not changed\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Local file is newer .. are we in a --download-only situation?\n\t\t\t\t\t\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\t\t\t\t\t\t// Not a --download-only scenario\n\t\t\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\t\t\t// Attempt to update the timestamp in the correct location\n\t\t\t\t\t\t\t\t\t// We need to use the correct driveId and itemId, especially if we are updating a OneDrive Business Shared File timestamp\n\t\t\t\t\t\t\t\t\tif (dbItem.type == ItemType.file) {\n\t\t\t\t\t\t\t\t\t\t// Not a remote file\n\t\t\t\t\t\t\t\t\t\t// Where should the timestamp update be performed ?\n\t\t\t\t\t\t\t\t\t\tif (localModifiedTime >= itemModifiedTime) {\n\t\t\t\t\t\t\t\t\t\t\t// Log what is being done\n\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The local item has the same hash value as the item online but with a newer local timestamp - correcting online timestamp\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t\t// Correct timestamp\n\t\t\t\t\t\t\t\t\t\t\tuploadLastModifiedTime(dbItem, dbItem.driveId, dbItem.id, localModifiedTime.toUTC(), dbItem.eTag);\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t// Log what is being done\n\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The local item has the same hash value as the item online but with an older local timestamp - correcting local timestamp\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t\t// Set the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, localFilePath, dbItem.mtime);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Remote file, remote values need to be used, we may not even have permission to change timestamp, update local file\n\t\t\t\t\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t\t\t\t\t// Set the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, localFilePath, dbItem.mtime);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// --download-only being used\n\t\t\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t\t\t// Set the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, localFilePath, dbItem.mtime);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// The file has not changed\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The file has not changed\", [\"verbose\"]);}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t//The file is not readable - skipped\n\t\t\t\t\taddLogEntry(\"Skipping processing this file as it cannot be read (file permissions or file corruption): \" ~ localFilePath);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// The item was a file but now is a directory\n\t\t\t\tif (verboseLogging) {addLogEntry(\"The item was a file but now is a directory\", [\"verbose\"]);}\n\t\t\t}\n\t\t} else {\n\t\t\t// File does not exist locally, but is in our database as a dbItem containing all the data was passed into this function\n\t\t\t// If we are in a --dry-run situation - this file may never have existed as we never downloaded it\n\t\t\tif (!dryRun) {\n\t\t\t\t// Not --dry-run situation\n\t\t\t\tif (verboseLogging) {addLogEntry(\"The file has been deleted locally\", [\"verbose\"]);}\n\t\t\t\t// Add this to the array to handle post checking all database items\n\t\t\t\tdatabaseItemsToDeleteOnline ~= [DatabaseItemsToDeleteOnline(dbItem, localFilePath)];\n\t\t\t} else {\n\t\t\t\t// 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\n\t\t\t\t// Did we 'fake create it' as part of --dry-run ?\n\t\t\t\tbool idsFakedMatch = false;\n\t\t\t\t\n\t\t\t\t// Check the file id - was this faked\n\t\t\t\tforeach (i; idsFaked) {\n\t\t\t\t\tif (i[1] == dbItem.id) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Matched faked file which is 'supposed' to exist locally but not created|renamed due to --dry-run use\", [\"debug\"]);}\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The file has not changed\", [\"verbose\"]);}\n\t\t\t\t\t\tidsFakedMatch = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Check if the parent folder was faked being changed in any way .. so we need to check the parent id\n\t\t\t\tforeach (i; idsFaked) {\n\t\t\t\t\tif (i[1] == dbItem.parentId) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Matched faked parental directory which is 'supposed' to exist locally but not created|renamed due to --dry-run use\", [\"debug\"]);}\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The file has not changed\", [\"verbose\"]);}\n\t\t\t\t\t\tidsFakedMatch = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// file id or parent id of the file did not match anything we faked changing due to --dry-run\n\t\t\t\tif (!idsFakedMatch) {\n\t\t\t\t\t// 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\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The file has been deleted locally\", [\"verbose\"]);}\n\t\t\t\t\t// Add this to the array to handle post checking all database items\n\t\t\t\t\tdatabaseItemsToDeleteOnline ~= [DatabaseItemsToDeleteOnline(dbItem, localFilePath)];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Perform the database consistency check on this directory item\n\tvoid checkDirectoryDatabaseItemForConsistency(Item dbItem, string localFilePath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// What is the source of this item data?\n\t\tstring itemSource = \"database\";\n\t\t\n\t\t// Does this item|directory still exist on disk?\n\t\tif (exists(localFilePath)) {\n\t\t\t// Fix https://github.com/abraunegg/onedrive/issues/1915\n\t\t\ttry {\n\t\t\t\tif (!isDir(localFilePath)) {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The item was a directory but now it is a file\", [\"verbose\"]);}\n\t\t\t\t\tuploadDeletedItem(dbItem, localFilePath);\n\t\t\t\t\tuploadNewFile(localFilePath);\n\t\t\t\t} else {\n\t\t\t\t\t// Directory still exists locally\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The directory has not changed\", [\"verbose\"]);}\n\t\t\t\t\t// When we are using --single-directory, we use the getChildren() call to get all children of a path, meaning all children are already traversed\n\t\t\t\t\t// Thus, if we traverse the path of this directory .. we end up with double processing & log output .. which is not ideal\n\t\t\t\t\tif (!singleDirectoryScope) {\n\t\t\t\t\t\t// loop through the children\n\t\t\t\t\t\tItem[] childrenFromDatabase = itemDB.selectChildren(dbItem.driveId, dbItem.id);\n\t\t\t\t\t\tforeach (Item child; childrenFromDatabase) {\n\t\t\t\t\t\t\tcheckDatabaseItemForConsistency(child);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Clear DB response array\n\t\t\t\t\t\tchildrenFromDatabase = [];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (FileException e) {\n\t\t\t\t// display the error message\n\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, localFilePath);\n\t\t\t}\n\t\t} else {\n\t\t\t// Directory does not exist locally, but it is in our database as a dbItem containing all the data was passed into this function\n\t\t\t// If we are in a --dry-run situation - this directory may never have existed as we never created it\n\t\t\tif (!dryRun) {\n\t\t\t\t// Not --dry-run situation\n\t\t\t\tif (!appConfig.getValueBool(\"monitor\")) {\n\t\t\t\t\t// Not in --monitor mode\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The directory has been deleted locally\", [\"verbose\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// Appropriate message as we are in --monitor mode\n\t\t\t\t\tif (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\"]);}\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Most likely cause - 'inotify' event was missing for whatever action was taken locally or action taken when application was stopped\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t\t// A moved directory will be uploaded as 'new', delete the old directory and database reference\n\t\t\t\t// Add this to the array to handle post checking all database items\n\t\t\t\tdatabaseItemsToDeleteOnline ~= [DatabaseItemsToDeleteOnline(dbItem, localFilePath)];\n\t\t\t} else {\n\t\t\t\t// 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\n\t\t\t\t// Did we 'fake create it' as part of --dry-run ?\n\t\t\t\tbool idsFakedMatch = false;\n\t\t\t\tforeach (i; idsFaked) {\n\t\t\t\t\tif (i[1] == dbItem.id) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Matched faked directory which is 'supposed' to exist locally but not created|renamed due to --dry-run use\", [\"debug\"]);}\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The directory has not changed\", [\"verbose\"]);}\n\t\t\t\t\t\tidsFakedMatch = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!idsFakedMatch) {\n\t\t\t\t\t// 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\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The directory has been deleted locally\", [\"verbose\"]);}\n\t\t\t\t\t// Add this to the array to handle post checking all database items\n\t\t\t\t\tdatabaseItemsToDeleteOnline ~= [DatabaseItemsToDeleteOnline(dbItem, localFilePath)];\n\t\t\t\t} else {\n\t\t\t\t\t// 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\n\t\t\t\t\t// Thus, if we traverse the path of this directory .. we end up with double processing & log output .. which is not ideal\n\t\t\t\t\tif (!singleDirectoryScope) {\n\t\t\t\t\t\t// loop through the children\n\t\t\t\t\t\tItem[] childrenFromDatabase = itemDB.selectChildren(dbItem.driveId, dbItem.id);\n\t\t\t\t\t\tforeach (Item child; childrenFromDatabase) {\n\t\t\t\t\t\t\tcheckDatabaseItemForConsistency(child);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Clear DB response array\n\t\t\t\t\t\tchildrenFromDatabase = [];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// 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.\n\tbool checkPathAgainstMicrosoftNamingRestrictions(string localFilePath, string logModifier = \"item\") {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\n\t\t// Check if the given path violates certain Microsoft restrictions and limitations\n\t\t// Return a true|false response\n\t\tbool invalidPath = false;\n\t\t\n\t\t// Check path against Microsoft OneDrive restriction and limitations about Windows naming for files and folders\n\t\tif (!invalidPath) {\n\t\t\tif (!isValidName(localFilePath)) { // This will return false if this is not a valid name according to the OneDrive API specifications\n\t\t\t\taddLogEntry(\"Skipping \" ~ logModifier ~\" - invalid name (Microsoft Naming Convention): \" ~ localFilePath, [\"info\", \"notify\"]);\n\t\t\t\tinvalidPath = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Check path for bad whitespace items\n\t\tif (!invalidPath) {\n\t\t\tif (containsBadWhiteSpace(localFilePath)) { // This will return true if this contains a bad whitespace character\n\t\t\t\taddLogEntry(\"Skipping \" ~ logModifier ~\" - invalid name (Contains an invalid whitespace character): \" ~ localFilePath, [\"info\", \"notify\"]);\n\t\t\t\tinvalidPath = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Check path for HTML ASCII Codes\n\t\tif (!invalidPath) {\n\t\t\tif (containsASCIIHTMLCodes(localFilePath)) { // This will return true if this contains HTML ASCII Codes\n\t\t\t\taddLogEntry(\"Skipping \" ~ logModifier ~\" - invalid name (Contains HTML ASCII Code): \" ~ localFilePath, [\"info\", \"notify\"]);\n\t\t\t\tinvalidPath = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Validate that the path is a valid UTF-16 encoded path\n\t\tif (!invalidPath) {\n\t\t\tif (!isValidUTF16(localFilePath)) { // This will return true if this is a valid UTF-16 encoded path, so we are checking for 'false' as response\n\t\t\t\taddLogEntry(\"Skipping \" ~ logModifier ~\" - invalid name (Invalid UTF-16 encoded path): \" ~ localFilePath, [\"info\", \"notify\"]);\n\t\t\t\tinvalidPath = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Check path for ASCII Control Codes\n\t\tif (!invalidPath) {\n\t\t\tif (containsASCIIControlCodes(localFilePath)) { // This will return true if this contains ASCII Control Codes\n\t\t\t\taddLogEntry(\"Skipping \" ~ logModifier ~\" - invalid name (Contains ASCII Control Codes): \" ~ localFilePath, [\"info\", \"notify\"]);\n\t\t\t\tinvalidPath = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return if this is a valid path\n\t\treturn invalidPath;\n\t}\n\t\n\t// Does this local path (directory or file) get excluded from any operation based on any client side filtering rules?\n\tbool checkPathAgainstClientSideFiltering(string localFilePath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Check the path against client side filtering rules\n\t\t// - check_nosync\n\t\t// - skip_dotfiles\n\t\t// - skip_symlinks\n\t\t// - skip_file\n\t\t// - skip_dir\n\t\t// - sync_list\n\t\t// - skip_size\n\t\t// Return a true|false response\n\t\tbool clientSideRuleExcludesPath = false;\n\t\t\n\t\t// Reset global syncListDirExcluded\n\t\tsyncListDirExcluded = false;\n\t\t\n\t\t// does the path exist?\n\t\tif (!exists(localFilePath)) {\n\t\t\t// path does not exist - we cant review any client side rules on something that does not exist locally\n\t\t\treturn clientSideRuleExcludesPath;\n\t\t}\n\t\n\t\t// - check_nosync\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\t// Do we need to check for .nosync? Only if --check-for-nosync was passed in\n\t\t\tif (appConfig.getValueBool(\"check_nosync\")) {\n\t\t\t\tif (exists(localFilePath ~ \"/.nosync\")) {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping item - .nosync found & --check-for-nosync enabled: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// - skip_dotfiles\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\t// Do we need to check skip dot files if configured\n\t\t\tif (appConfig.getValueBool(\"skip_dotfiles\")) {\n\t\t\t\tif (isDotFile(localFilePath)) {\n\t\t\t\t\tif (!syncListConfigured) {\n\t\t\t\t\t\t// 'sync_list' is not in use\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping item - .file or .folder: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// 'sync_list' is in use - potentially skipping .file or .folder but it may be included via 'sync_list'\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Potentially skipping item - .file or .folder (sync_list inclusion check to be done): \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// - skip_symlinks\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\t// Is the path a symbolic link\n\t\t\tif (isSymlink(localFilePath)) {\n\t\t\t\t// if config says so we skip all symlinked items\n\t\t\t\tif (appConfig.getValueBool(\"skip_symlinks\")) {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping item - skip symbolic links configured: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t}\n\t\t\t\t// skip unexisting symbolic links\n\t\t\t\telse if (!exists(readLink(localFilePath))) {\n\t\t\t\t\t// reading the symbolic link failed - is the link a relative symbolic link\n\t\t\t\t\t//   drwxrwxr-x. 2 alex alex 46 May 30 09:16 .\n\t\t\t\t\t//   drwxrwxr-x. 3 alex alex 35 May 30 09:14 ..\n\t\t\t\t\t//   lrwxrwxrwx. 1 alex alex 61 May 30 09:16 absolute.txt -> /home/alex/OneDrivePersonal/link_tests/intercambio/prueba.txt\n\t\t\t\t\t//   lrwxrwxrwx. 1 alex alex 13 May 30 09:16 relative.txt -> ../prueba.txt\n\t\t\t\t\t//\n\t\t\t\t\t// 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'\n\t\t\t\t\tstring currentSyncDir = getcwd();\n\t\t\t\t\tstring fullLinkPath = buildNormalizedPath(absolutePath(localFilePath));\n\t\t\t\t\tstring fileName = baseName(fullLinkPath);\n\t\t\t\t\tstring parentLinkPath = dirName(fullLinkPath);\n\t\t\t\t\t// test if this is a 'relative' symbolic link\n\t\t\t\t\tchdir(parentLinkPath);\n\t\t\t\t\tauto relativeLink = readLink(fileName);\n\t\t\t\t\tauto relativeLinkTest = exists(readLink(fileName));\n\t\t\t\t\t// reset back to our 'sync_dir'\n\t\t\t\t\tchdir(currentSyncDir);\n\t\t\t\t\t// results\n\t\t\t\t\tif (relativeLinkTest) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Not skipping item - symbolic link is a 'relative link' to target ('\" ~ relativeLink ~ \"') which can be supported: \" ~ localFilePath, [\"debug\"]);}\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"Skipping item - invalid symbolic link: \"~ localFilePath, [\"info\", \"notify\"]);\n\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Is this item excluded by user configuration of skip_dir or skip_file?\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\tif (localFilePath != \".\") {\n\t\t\t\t// skip_dir handling\n\t\t\t\tif (isDir(localFilePath)) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Checking local path: \" ~ localFilePath, [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Only check path if config is != \"\"\n\t\t\t\t\tif (appConfig.getValueString(\"skip_dir\") != \"\") {\n\t\t\t\t\t\t// The path that needs to be checked needs to include the '/'\n\t\t\t\t\t\t// This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched\n\t\t\t\t\t\tif (selectiveSync.isDirNameExcluded(localFilePath.strip('.'))) {\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by skip_dir config: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// skip_file handling\n\t\t\t\tif (isFile(localFilePath)) {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Checking file: \" ~ localFilePath, [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// The path that needs to be checked needs to include the '/'\n\t\t\t\t\t// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched\n\t\t\t\t\tif (selectiveSync.isFileNameExcluded(localFilePath.strip('.'))) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - excluded by skip_file config: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\n\t\t// Is this item excluded by user configuration of sync_list?\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\tif (localFilePath != \".\") {\n\t\t\t\tif (syncListConfigured) {\n\t\t\t\t\t// sync_list configured and in use\n\t\t\t\t\tif (selectiveSync.isPathExcludedViaSyncList(localFilePath)) {\n\t\t\t\t\t\tif ((isFile(localFilePath)) && (appConfig.getValueBool(\"sync_root_files\")) && (rootName(localFilePath.strip('.').strip('/')) == \"\")) {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Not skipping path due to sync_root_files inclusion: \" ~ localFilePath, [\"debug\"]);}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif (exists(appConfig.syncListFilePath)){\n\t\t\t\t\t\t\t\t// skipped most likely due to inclusion in sync_list\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// is this path a file or directory?\n\t\t\t\t\t\t\t\tif (isFile(localFilePath)) {\n\t\t\t\t\t\t\t\t\t// file\t\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - excluded by sync_list config: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// directory\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by sync_list config: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t// update syncListDirExcluded\n\t\t\t\t\t\t\t\t\tsyncListDirExcluded = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// flag as excluded\n\t\t\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// skipped for some other reason\n\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by user config: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Check if this is excluded by a user set maximum filesize to upload\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\tif (isFile(localFilePath)) {\n\t\t\t\tif (fileSizeLimit != 0) {\n\t\t\t\t\t// Get the file size\n\t\t\t\t\tlong thisFileSize = getSize(localFilePath);\n\t\t\t\t\tif (thisFileSize >= fileSizeLimit) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - excluded by skip_size config: \" ~ localFilePath ~ \" (\" ~ to!string(thisFileSize/2^^20) ~ \" MB)\", [\"verbose\"]);}\n\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return if path is excluded\n\t\treturn clientSideRuleExcludesPath;\n\t}\n\t\n\t// Does this JSON item (as received from OneDrive API) get excluded from any operation based on any client side filtering rules?\n\t// 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\n\tbool checkJSONAgainstClientSideFiltering(JSONValue onedriveJSONItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Debug what JSON we are evaluating against Client Side Filtering Rules\n\t\tif (debugLogging) {addLogEntry(\"Checking this JSON against Client Side Filtering Rules: \" ~ sanitiseJSONItem(onedriveJSONItem), [\"debug\"]);}\n\t\t\t\t\n\t\t// Function flag\n\t\tbool clientSideRuleExcludesPath = false;\n\t\t\n\t\t// Check the path against client side filtering rules\n\t\t// - check_nosync (MISSING)\n\t\t// - skip_dotfiles (MISSING)\n\t\t// - skip_symlinks (MISSING)\n\t\t// - skip_dir \n\t\t// - skip_file\n\t\t// - sync_list\n\t\t// - skip_size\n\t\t// Return a true|false response\n\t\t\n\t\t// Use the JSON elements rather than computing a DB struct via makeItem()\n\t\tstring thisItemId = onedriveJSONItem[\"id\"].str;\n\t\tstring thisItemDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\tstring thisItemParentId = onedriveJSONItem[\"parentReference\"][\"id\"].str;\n\t\tstring thisItemName = onedriveJSONItem[\"name\"].str;\n\t\t\n\t\t// Issue #3336 - Convert thisItemDriveId to lowercase before any test\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\tthisItemDriveId = transformToLowerCase(thisItemDriveId);\n\t\t}\n\t\t\n\t\t// Is this parent is in the database\n\t\tbool parentInDatabase = false;\n\t\tstring calculatedParentalPath;\n\t\t\n\t\t// Calculate if the Parent Item is in the database so that this flag can be reused\n\t\tparentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId);\n\t\tif (parentInDatabase) {\n\t\t\t// Calculate this items path based on database entries\n\t\t\tif (debugLogging) {addLogEntry(\"Parent path details are in DB - computing 'calculatedParentalPath' using computeItemPath()\", [\"debug\"]);}\n\t\t\tcalculatedParentalPath = computeItemPath(thisItemDriveId, thisItemParentId);\n\t\t\tif (debugLogging) {addLogEntry(\"Resulting 'calculatedParentalPath' using computeItemPath() = \" ~ calculatedParentalPath, [\"debug\"]);}\n\t\t}\n\t\t\n\t\t// Check if this is excluded by config option: skip_dir \n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\t// Is the item a folder?\n\t\t\tif (isItemFolder(onedriveJSONItem)) {\n\t\t\t\t// Only check path if config is != \"\"\n\t\t\t\tif (!appConfig.getValueString(\"skip_dir\").empty) {\n\t\t\t\t\t// work out the 'snippet' path where this folder would be created\n\t\t\t\t\tstring simplePathToCheck = \"\";\n\t\t\t\t\tstring complexPathToCheck = \"\";\n\t\t\t\t\tstring matchDisplay = \"\";\n\t\t\t\t\t\n\t\t\t\t\tif (hasParentReference(onedriveJSONItem)) {\n\t\t\t\t\t\t// we need to workout the FULL path for this item\n\t\t\t\t\t\t// simple path\n\t\t\t\t\t\tif ((\"name\" in onedriveJSONItem[\"parentReference\"]) != null) {\n\t\t\t\t\t\t\tsimplePathToCheck = onedriveJSONItem[\"parentReference\"][\"name\"].str ~ \"/\" ~ onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsimplePathToCheck = onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir path to check (simple):  \" ~ simplePathToCheck, [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// complex path calculation\n\t\t\t\t\t\tif (parentInDatabase) {\n\t\t\t\t\t\t\t// build up complexPathToCheck based on database data\n\t\t\t\t\t\t\tcomplexPathToCheck = calculatedParentalPath ~ \"/\" ~ thisItemName;\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updated 'complexPathToCheck' to '\"~ complexPathToCheck ~\"' for 'skip_dir' validation to determine if this directory should be excluded.\", [\"debug\"]);}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Parent details not in database - unable to compute complex path to check using database data\", [\"debug\"]);}\n\t\t\t\t\t\t\t// use onedriveJSONItem[\"parentReference\"][\"path\"].str\n\t\t\t\t\t\t\tstring selfBuiltPath = onedriveJSONItem[\"parentReference\"][\"path\"].str ~ \"/\" ~ onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Check for ':' and split if present\n\t\t\t\t\t\t\tauto splitIndex = selfBuiltPath.indexOf(\":\");\n\t\t\t\t\t\t\tif (splitIndex != -1) {\n\t\t\t\t\t\t\t\t// Keep only the part after ':'\n\t\t\t\t\t\t\t\tselfBuiltPath = selfBuiltPath[splitIndex + 1 .. $];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// set complexPathToCheck to selfBuiltPath and be compatible with computeItemPath() output\n\t\t\t\t\t\t\tcomplexPathToCheck = \".\" ~ selfBuiltPath;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// were we able to compute a complexPathToCheck ?\n\t\t\t\t\t\tif (!complexPathToCheck.empty) {\n\t\t\t\t\t\t\t// complexPathToCheck must at least start with './' to ensure logging output consistency but also for pattern matching consistency\n\t\t\t\t\t\t\tif (!startsWith(complexPathToCheck, \"./\")) {\n\t\t\t\t\t\t\t\tcomplexPathToCheck = \"./\" ~ complexPathToCheck;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// log the complex path to check\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir path to check (complex): \" ~ complexPathToCheck, [\"debug\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsimplePathToCheck = onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// If 'simplePathToCheck' or 'complexPathToCheck' is of the following format:  root:/folder\n\t\t\t\t\t// then isDirNameExcluded matching will not work\n\t\t\t\t\tif (simplePathToCheck.canFind(\":\")) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updating simplePathToCheck to remove 'root:'\", [\"debug\"]);}\n\t\t\t\t\t\tsimplePathToCheck = processPathToRemoveRootReference(simplePathToCheck);\n\t\t\t\t\t}\n\t\t\t\t\tif (complexPathToCheck.canFind(\":\")) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updating complexPathToCheck to remove 'root:'\", [\"debug\"]);}\n\t\t\t\t\t\tcomplexPathToCheck = processPathToRemoveRootReference(complexPathToCheck);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// OK .. what checks are we doing?\n\t\t\t\t\tif ((!simplePathToCheck.empty) && (complexPathToCheck.empty)) {\n\t\t\t\t\t\t// just a simple check\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Performing a simple check only\", [\"debug\"]);}\n\t\t\t\t\t\tclientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(simplePathToCheck);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// simple and complex\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Performing a simple then complex path match if required\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t// simple first\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Performing a simple check first\", [\"debug\"]);}\n\t\t\t\t\t\tclientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(simplePathToCheck);\n\t\t\t\t\t\tif (!clientSideRuleExcludesPath) {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Simple match was false, attempting complex match\", [\"debug\"]);}\n\t\t\t\t\t\t\t// simple didnt match, perform a complex check\n\t\t\t\t\t\t\tclientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(complexPathToCheck);\t\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// End Result\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir exclude result (directory based): \" ~ to!string(clientSideRuleExcludesPath), [\"debug\"]);}\n\t\t\t\t\tif (clientSideRuleExcludesPath) {\n\t\t\t\t\t\t// what path should be displayed if we are excluding\n\t\t\t\t\t\tif (!complexPathToCheck.empty) {\n\t\t\t\t\t\t\t// try and always use the complex path as it is more complete for application output\n\t\t\t\t\t\t\tmatchDisplay = complexPathToCheck;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tmatchDisplay = simplePathToCheck;\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t\t// This path should be skipped\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by skip_dir config: \" ~ matchDisplay, [\"verbose\"]);}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Is the item a file?\n\t\t\t// We need to check to see if this files path is excluded as well\n\t\t\tif (isItemFile(onedriveJSONItem)) {\n\t\t\t\n\t\t\t\t// Only check path if config is != \"\"\n\t\t\t\tif (!appConfig.getValueString(\"skip_dir\").empty) {\n\t\t\t\t\t// variable to check the file path against skip_dir\n\t\t\t\t\tstring pathToCheck;\n\t\t\t\t\t\n\t\t\t\t\tif (parentInDatabase) {\n\t\t\t\t\t\t// Parent is in the database - use those details to compute this files parental path\n\t\t\t\t\t\tpathToCheck = calculatedParentalPath;\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updated 'pathToCheck' to '\"~ pathToCheck ~\"' for 'skip_dir' validation to determine if this file should be excluded.\", [\"debug\"]);}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Parent is not in the database .. compute manually\n\t\t\t\t\t\tif (hasParentReference(onedriveJSONItem)) {\n\t\t\t\t\t\t\t// use onedriveJSONItem[\"parentReference\"][\"path\"].str\n\t\t\t\t\t\t\tstring selfBuiltPath = onedriveJSONItem[\"parentReference\"][\"path\"].str;\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Initial file based selfBuiltPath = \" ~ selfBuiltPath, [\"debug\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Check for ':' and split if present within 'selfBuiltPath'\n\t\t\t\t\t\t\tauto splitIndex = selfBuiltPath.indexOf(\":\");\n\t\t\t\t\t\t\tif (splitIndex != -1) {\n\t\t\t\t\t\t\t\t// Keep only the part after ':'\n\t\t\t\t\t\t\t\tstring pathAfterSplit = selfBuiltPath[splitIndex + 1 .. $];\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"pathAfterSplit = \" ~ pathAfterSplit, [\"debug\"]);}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif (pathAfterSplit.empty) {\n\t\t\t\t\t\t\t\t\t// Empty path, thus this is most likely a file in the account root\n\t\t\t\t\t\t\t\t\tselfBuiltPath = \"/\";\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// There is a path after the split, this is the path we are interested in\n\t\t\t\t\t\t\t\t\t// However ... in a Shared Folder scenario, this path now is the absolute path on the remote driveID .. could be problematic\n\t\t\t\t\t\t\t\t\tselfBuiltPath = pathAfterSplit;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Result after split\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"selfBuiltPath after splitting at : = \" ~ selfBuiltPath, [\"debug\"]);}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Update file path to check against 'skip_dir' using the self built details\n\t\t\t\t\t\t\tpathToCheck = selfBuiltPath;\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updated (manual computation) 'pathToCheck' to '\"~ pathToCheck ~\"' for 'skip_dir' validation to determine if this file should be excluded.\", [\"debug\"]);}\n\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t}\t\n\t\t\t\t\t\n\t\t\t\t\t// Build the consistent path for logging output\n\t\t\t\t\tstring logItemPath = ensureStartsWithDotSlash(buildNormalizedPath(pathToCheck ~ \"/\" ~ onedriveJSONItem[\"name\"].str));\n\t\t\t\t\t\n\t\t\t\t\t// Perform the skip_dir check for file path\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir path to check (file based): \" ~ to!string(pathToCheck), [\"debug\"]);}\n\t\t\t\t\tclientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(pathToCheck);\n\t\t\t\t\t\n\t\t\t\t\t// 'skip_dir' result\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_dir exclude result (file based): \" ~ to!string(clientSideRuleExcludesPath), [\"debug\"]);}\n\t\t\t\t\tif (clientSideRuleExcludesPath) {\n\t\t\t\t\t\t// this files path should be skipped\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - file path is excluded by skip_dir config: \" ~ logItemPath, [\"verbose\"]);}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Check if this is excluded by config option: skip_file\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\t// is the item a file ?\n\t\t\tif (isFileItem(onedriveJSONItem)) {\n\t\t\t\t// JSON item is a file\n\t\t\t\t\n\t\t\t\t// skip_file can contain 4 types of entries:\n\t\t\t\t// - wildcard - *.txt\n\t\t\t\t// - text + wildcard - name*.txt\n\t\t\t\t// - full path + combination of any above two - /path/name*.txt\n\t\t\t\t// - full path to file - /path/to/file.txt\n\t\t\t\t\n\t\t\t\tstring exclusionTestPath = \"\";\n\t\t\t\t\n\t\t\t\t// is the parent id in the database?\n\t\t\t\tif (parentInDatabase) {\n\t\t\t\t\t// parent id is in the database, so we can try and calculate the full file path\n\t\t\t\t\tstring newItemPath = \"\";\n\t\t\t\t\t\n\t\t\t\t\t// Compute this item path & need the full path for this file\n\t\t\t\t\tnewItemPath = calculatedParentalPath ~ \"/\" ~ thisItemName;\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t// The path that needs to be checked needs to include the '/'\n\t\t\t\t\t// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched\n\t\t\t\t\t// However, as 'path' used throughout, use a temp variable with this modification so that we use the temp variable for exclusion checks\n\t\t\t\t\tif (!startsWith(newItemPath, \"/\")){\n\t\t\t\t\t\t// Add '/' to the path\n\t\t\t\t\t\texclusionTestPath = '/' ~ newItemPath;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Normalise the path to ensure any initial sequence of '/./././' or similar is normalised\n\t\t\t\t\texclusionTestPath = buildNormalizedPath(exclusionTestPath);\n\t\t\t\t\t\n\t\t\t\t\t// what are we checking\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updated 'newItemPath' to '\"~ newItemPath ~\"' for 'skip_file' validation to determine if this file should be excluded.\", [\"debug\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// parent not in database, we can only check using this JSON item's name\n\t\t\t\t\tif (!startsWith(thisItemName, \"/\")){\n\t\t\t\t\t\t// Add '/' to the path\n\t\t\t\t\t\texclusionTestPath = '/' ~ thisItemName;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// what are we checking\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"skip_file item to check (file name only - parent path not in database): \" ~ exclusionTestPath, [\"debug\"]);}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Perform the 'skip_file' evaluation\n\t\t\t\tclientSideRuleExcludesPath = selectiveSync.isFileNameExcluded(exclusionTestPath);\n\t\t\t\tif (debugLogging) {addLogEntry(\"skip_file evaluation result: \" ~ to!string(clientSideRuleExcludesPath), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\tif (clientSideRuleExcludesPath) {\n\t\t\t\t\t// This path should be skipped\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - excluded by skip_file config: \" ~ exclusionTestPath, [\"verbose\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\t\n\t\t// Check if this is included or excluded by use of sync_list\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\t// No need to try and process something against a sync_list if it has been configured\n\t\t\tif (syncListConfigured) {\n\t\t\t\t// Compute the item path if empty - as to check sync_list we need an actual path to check\n\t\t\t\t// What is the path of the new item\n\t\t\t\tstring newItemPath;\n\t\t\t\t\n\t\t\t\t// Is the parent in the database? If not, we cannot compute the full path based on the database entries\n\t\t\t\t// In a --resync scenario - the database is empty\n\t\t\t\tif (parentInDatabase) {\n\t\t\t\t\t// Calculate this items path based on database entries\n\t\t\t\t\tnewItemPath = calculatedParentalPath ~ \"/\" ~ thisItemName;\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Updated 'newItemPath' to '\"~ newItemPath ~\"' for 'sync_list' validation to determine if this directory should be included.\", [\"debug\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// Parent is not in the database .. we need to compute it .. why ????\n\t\t\t\t\tif (appConfig.getValueBool(\"resync\")) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Parent NOT in DB .. we need to manually compute this path due to --resync being used\", [\"debug\"]);}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Parent NOT in DB .. we need to manually compute this path .......\", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// gather the applicable path details\n\t\t\t\t\tif ((\"path\" in onedriveJSONItem[\"parentReference\"]) != null) {\n\t\t\t\t\t\t// If there is a parent reference path, try and use it\n\t\t\t\t\t\tstring selfBuiltPath = onedriveJSONItem[\"parentReference\"][\"path\"].str ~ \"/\" ~ onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Check for ':' and split if present\n\t\t\t\t\t\tstring[] splitPaths;\n\t\t\t\t\t\tauto splitIndex = selfBuiltPath.indexOf(\":\");\n\t\t\t\t\t\tif (splitIndex != -1) {\n\t\t\t\t\t\t\t// Keep only the part after ':'\n\t\t\t\t\t\t\tsplitPaths = selfBuiltPath.split(\":\");\n\t\t\t\t\t\t\tselfBuiltPath = splitPaths[1];\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Debug output what the self-built path currently is\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\" - selfBuiltPath currently calculated as: \" ~ selfBuiltPath, [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Issue #2731\n\t\t\t\t\t\t// Get the remoteDriveId from JSON record\n\t\t\t\t\t\tstring remoteDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\tremoteDriveId = transformToLowerCase(remoteDriveId);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is this potentially a shared folder? This is the only reliable way to determine this ...\n\t\t\t\t\t\tif (remoteDriveId != appConfig.defaultDriveId) {\n\t\t\t\t\t\t\t// Yes this JSON is from a Shared Folder\n\t\t\t\t\t\t\t// Query the database for the 'remote' folder details from the database\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Query database for this 'remoteDriveId' record: \" ~ to!string(remoteDriveId), [\"debug\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tItem remoteItem;\n\t\t\t\t\t\t\titemDB.selectByRemoteDriveId(remoteDriveId, remoteItem);\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Query returned result (itemDB.selectByRemoteDriveId): \" ~ to!string(remoteItem), [\"debug\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Shared Folders present a unique challenge to determine what path needs to be used, especially in a --resync scenario where there are near zero records available to use computeItemPath() \n\t\t\t\t\t\t\t// Update the path that will be used to check 'sync_list' with the 'name' of the remoteDriveId database record\n\t\t\t\t\t\t\t// Issue #3331\n\t\t\t\t\t\t\t// Avoid duplicating the shared folder root name if already present\n\t\t\t\t\t\t\tif (!selfBuiltPath.startsWith(\"/\" ~ remoteItem.name ~ \"/\")) {\n\t\t\t\t\t\t\t\tselfBuiltPath = remoteItem.name ~ selfBuiltPath;\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"selfBuiltPath after 'Shared Folder' DB details update = \" ~ to!string(selfBuiltPath), [\"debug\"]);}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Shared Folder name already present in path; no update needed to selfBuiltPath\", [\"debug\"]);}\t\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Issue #2740\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\t// Does the path contain HTML encoding?\n\t\t\t\t\t\tif (containsURLEncodedItems(selfBuiltPath)) {\n\t\t\t\t\t\t\t// decode it\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"selfBuiltPath for sync_list check needs decoding: \" ~ selfBuiltPath, [\"debug\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t// try and decode selfBuiltPath\n\t\t\t\t\t\t\t\tnewItemPath = decodeComponent(selfBuiltPath);\n\t\t\t\t\t\t\t} catch (URIException exception) {\n\t\t\t\t\t\t\t\t// why?\n\t\t\t\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Unable to URL Decode path: \" ~ exception.msg, [\"verbose\"]);\n\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: To resolve, rename this item online: \" ~ selfBuiltPath, [\"verbose\"]);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// have to use as-is due to decode error\n\t\t\t\t\t\t\t\tnewItemPath = selfBuiltPath;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// use as-is\n\t\t\t\t\t\t\tnewItemPath = selfBuiltPath;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// The final format of newItemPath when self building needs to be the same as newItemPath when computed using computeItemPath .. this is handled later below\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"newItemPath as manually computed by selfBuiltPath process = \" ~ to!string(selfBuiltPath), [\"debug\"]);}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// no parent reference path available in provided JSON\n\t\t\t\t\t\tnewItemPath = thisItemName;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// The 'newItemPath' needs to be updated to ensure it is in the right format\n\t\t\t\t// Regardless of built from DB or computed it needs to be in this format:\n\t\t\t\t//   ./path/path/ etc\n\t\t\t\t// This then makes the path output with 'sync_list' consistent, and, more importantly consistent for 'sync_list' evaluations\n\t\t\t\tnewItemPath = ensureStartsWithDotSlash(newItemPath);\n\t\t\t\t\t\t\t\t\n\t\t\t\t// Check for HTML entities (e.g., '%20' for space) in newItemPath\n\t\t\t\tif (containsURLEncodedItems(newItemPath)) {\n\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\taddLogEntry(\"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.\", [\"verbose\"]);\n\t\t\t\t\t\taddLogEntry(\"WORKAROUND: An alternative solution could be to change the name of this item through the online platform: \" ~ newItemPath, [\"verbose\"]);\n\t\t\t\t\t\taddLogEntry(\"See: https://github.com/OneDrive/onedrive-api-docs/issues/1765 for further details\", [\"verbose\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// What path are we checking against sync_list?\n\t\t\t\tif (debugLogging) {addLogEntry(\"Path to check against 'sync_list' entries: \" ~ newItemPath, [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Unfortunately there is no avoiding this call to check if the path is excluded|included via sync_list\n\t\t\t\tif (selectiveSync.isPathExcludedViaSyncList(newItemPath)) {\n\t\t\t\t\t// selective sync advised to skip, however is this a file and are we configured to upload / download files in the root?\n\t\t\t\t\tif ((isItemFile(onedriveJSONItem)) && (appConfig.getValueBool(\"sync_root_files\")) && (rootName(newItemPath) == \"\") ) {\n\t\t\t\t\t\t// This is a file\n\t\t\t\t\t\t// We are configured to sync all files in the root\n\t\t\t\t\t\t// This is a file in the logical root\n\t\t\t\t\t\tclientSideRuleExcludesPath = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Path is unwanted, flag to exclude\n\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Has this itemId already been flagged as being skipped?\n\t\t\t\t\t\tif (!syncListSkippedParentIds.canFind(thisItemId)) {\n\t\t\t\t\t\t\tif (isItemFolder(onedriveJSONItem)) {\n\t\t\t\t\t\t\t\t// Detail we are skipping this JSON data from online\n\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping path - excluded by sync_list config: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t// Add this folder id to the elements we have already detailed we are skipping, so we do no output this again\n\t\t\t\t\t\t\t\tsyncListSkippedParentIds ~= thisItemId;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is this is a 'add shortcut to onedrive' link?\n\t\t\t\t\t\tif (isItemRemote(onedriveJSONItem)) {\n\t\t\t\t\t\t\t// Detail we are skipping this JSON data from online\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping Shared Folder Link - excluded by sync_list config: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t// Add this folder id to the elements we have already detailed we are skipping, so we do no output this again\n\t\t\t\t\t\t\tsyncListSkippedParentIds ~= thisItemId;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Is this a file or directory?\n\t\t\t\t\tif (isItemFile(onedriveJSONItem)) {\n\t\t\t\t\t\t// File included due to 'sync_list' match\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Including file - included by sync_list config: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is the parent item in the database?\n\t\t\t\t\t\tif (!parentInDatabase) {\n\t\t\t\t\t\t\t// Parental database structure needs to be created\n\t\t\t\t\t\t\tstring newParentalPath = dirName(newItemPath);\n\t\t\t\t\t\t\t// Log that this parental structure needs to be created\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Parental Path structure needs to be created to support included file: \" ~ newParentalPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t// Recursively, stepping backward from 'thisItemParentId', query online, save entry to DB and create the local path structure\n\t\t\t\t\t\t\tcreateLocalPathStructure(onedriveJSONItem, newParentalPath);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// If this is --dry-run\n\t\t\t\t\t\t\tif (dryRun) {\n\t\t\t\t\t\t\t\t// we dont create the directory, but we need to track that we 'faked it'\n\t\t\t\t\t\t\t\tidsFaked ~= [onedriveJSONItem[\"parentReference\"][\"driveId\"].str, onedriveJSONItem[\"parentReference\"][\"id\"].str];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Directory included due to 'sync_list' match\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Including path - included by sync_list config: \" ~ newItemPath, [\"verbose\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// So that this path is in the DB, we need to add onedriveJSONItem to the DB so that this record can be used to build paths if required\n\t\t\t\t\t\tif (parentInDatabase) {\n\t\t\t\t\t\t\t// Parent is in DB .. is this a 'new' object or an 'existing' object?\n\t\t\t\t\t\t\t// Issue #3501 - If an online name name is done, the item needs to be 'renamed' via applyPotentiallyChangedItem() later\n\t\t\t\t\t\t\t// Only save to the database at this point, if this JSON 'id' is not already in the database to allow applyPotentiallyChangedItem() to operate as expected\n\t\t\t\t\t\t\tItem tempDBItem;\n\t\t\t\t\t\t\titemDB.selectById(onedriveJSONItem[\"parentReference\"][\"driveId\"].str, onedriveJSONItem[\"id\"].str, tempDBItem);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Was a valid DB response returned\n\t\t\t\t\t\t\tif (tempDBItem.driveId.empty) {\n\t\t\t\t\t\t\t\t// No .. so this is a new item\n\t\t\t\t\t\t\t\t// Save this JSON now\n\t\t\t\t\t\t\t\tsaveItem(onedriveJSONItem);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Check if this is excluded by a user set maximum filesize to download\n\t\tif (!clientSideRuleExcludesPath) {\n\t\t\tif (isItemFile(onedriveJSONItem)) {\n\t\t\t\tif (fileSizeLimit != 0) {\n\t\t\t\t\tif (onedriveJSONItem[\"size\"].integer >= fileSizeLimit) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping file - excluded by skip_size config: \" ~ thisItemName ~ \" (\" ~ to!string(onedriveJSONItem[\"size\"].integer/2^^20) ~ \" MB)\", [\"verbose\"]);}\n\t\t\t\t\t\tclientSideRuleExcludesPath = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return if path is excluded\n\t\treturn clientSideRuleExcludesPath;\n\t}\n\t\n\t\n\t\n\t\n\t\n\t// Ensure the path passed in, is in the correct format to use when evaluating 'sync_list' rules\n\tstring ensureStartsWithDotSlash(string inputPath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Check if the path starts with './'\n\t\tif (inputPath.startsWith(\"./\")) {\n\t\t\treturn inputPath; // No modification needed\n\t\t}\n\n\t\t// Check if the path starts with '/' or does not start with '.' at all\n\t\tif (inputPath.startsWith(\"/\")) {\n\t\t\treturn \".\" ~ inputPath; // Prepend '.' to ensure it starts with './'\n\t\t}\n\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// If the path starts with any other character or is missing './', add './'\n\t\treturn \"./\" ~ inputPath;\n\t}\n\t\n\t// 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\n\tvoid createLocalPathStructure(JSONValue onedriveJSONItem, string newLocalParentalPath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Function variables\n\t\tbool parentInDatabase;\n\t\tJSONValue onlinePathData;\n\t\tOneDriveApi onlinePathOneDriveApiInstance;\n\t\tonlinePathOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tonlinePathOneDriveApiInstance.initialise();\n\t\tstring thisItemDriveId;\n\t\tstring thisItemParentId;\n\t\t\n\t\t// Log what we received to analyse\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"createLocalPathStructure input onedriveJSONItem: \" ~ to!string(onedriveJSONItem), [\"debug\"]);\n\t\t\taddLogEntry(\"createLocalPathStructure input newLocalParentalPath: \" ~ newLocalParentalPath, [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Configure these variables based on the JSON input\n\t\tthisItemDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\n\t\t// OneDrive Personal JSON responses are in-consistent with not having 'id' available\n\t\tif (hasParentReferenceId(onedriveJSONItem)) {\n\t\t\t// Use the parent reference id\n\t\t\tthisItemParentId = onedriveJSONItem[\"parentReference\"][\"id\"].str;\n\t\t}\n\t\t\n\t\t// To continue, thisItemDriveId and thisItemParentId must not be empty\n\t\tif ((thisItemDriveId != \"\") && (thisItemParentId != \"\")) {\n\t\t\t// Calculate if the Parent Item is in the database so that it can be re-used\n\t\t\tparentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId);\n\t\t\t\n\t\t\t// Is the parent in the database?\n\t\t\tif (!parentInDatabase) {\n\t\t\t\t// Get data from online for this driveId and JSON item parent .. so we have the parent details\n\t\t\t\tif (debugLogging) {addLogEntry(\"createLocalPathStructure parent is not in database, fetching parental details from online\", [\"debug\"]);}\n\t\t\t\ttry {\n\t\t\t\t\tonlinePathData = onlinePathOneDriveApiInstance.getPathDetailsById(thisItemDriveId, thisItemParentId);\n\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t// Display what the error is\n\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// There needs to be a valid JSON to process\n\t\t\t\tif (onlinePathData.type() == JSONType.object) {\n\t\t\t\t\t// Does this JSON match the root name of a shared folder we may be trying to match?\n\t\t\t\t\tif (sharedFolderDeltaGeneration) {\n\t\t\t\t\t\tif (currentSharedFolderName == onlinePathData[\"name\"].str) {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"createLocalPathStructure parent matches the current shared folder name, creating applicable shared folder database records\", [\"debug\"]);}\n\t\t\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(onlinePathData);\n\t\t\t\t\t\t}\n\t\t\t\t\t} \n\t\t\t\t\t\n\t\t\t\t\t// Configure the grandparent items\n\t\t\t\t\tstring grandparentItemDriveId;\n\t\t\t\t\tstring grandparentItemParentId;\n\t\t\t\t\tgrandparentItemDriveId = onlinePathData[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive Personal JSON responses are in-consistent with not having 'id' available\n\t\t\t\t\tif (hasParentReferenceId(onlinePathData)) {\n\t\t\t\t\t\t// Use the parent reference id\n\t\t\t\t\t\tgrandparentItemParentId = onlinePathData[\"parentReference\"][\"id\"].str;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Testing evidence shows that for Personal accounts, use the 'id' itself\n\t\t\t\t\t\tgrandparentItemParentId = onlinePathData[\"id\"].str;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Is this item's grandparent data in the database?\n\t\t\t\t\tif (!itemDB.idInLocalDatabase(grandparentItemDriveId, grandparentItemParentId)) {\n\t\t\t\t\t\t// grandparent needs to be added\n\t\t\t\t\t\tcreateLocalPathStructure(onlinePathData, dirName(newLocalParentalPath));\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// If this is --dry-run\n\t\t\t\t\tif (dryRun) {\n\t\t\t\t\t\t// we dont create the directory, but we need to track that we 'faked it'\n\t\t\t\t\t\tidsFaked ~= [grandparentItemDriveId, grandparentItemParentId];\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Does the parental path exist locally?\n\t\t\t\t\tif (!exists(newLocalParentalPath)) {\n\t\t\t\t\t\t// the required path does not exist locally - logging is done in handleLocalDirectoryCreation\n\t\t\t\t\t\t// create a db item record for the online data\n\t\t\t\t\t\tItem newDatabaseItem = makeItem(onlinePathData);\n\t\t\t\t\t\t// create the path locally, save the data to the database post path creation\n\t\t\t\t\t\thandleLocalDirectoryCreation(newDatabaseItem, newLocalParentalPath, onlinePathData);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// parent path exists locally, save the data to the database\n\t\t\t\t\t\tsaveItem(onlinePathData);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No valid JSON was responded with - unable to create local path structure\n\t\t\t\t\taddLogEntry(\"Unable to create the local path structure as the Microsoft OneDrive API returned an invalid response\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (debugLogging) {addLogEntry(\"createLocalPathStructure parent is in the database\", [\"debug\"]);}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tonlinePathOneDriveApiInstance.releaseCurlEngine();\n\t\tonlinePathOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Process the list of local changes to upload to OneDrive\n\tvoid processChangedLocalItemsToUpload() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Each element in this array 'databaseItemsWhereContentHasChanged' is an Database Item ID that has been modified locally\n\t\tsize_t batchSize = to!int(appConfig.getValueLong(\"threads\"));\n\t\tlong batchCount = (databaseItemsWhereContentHasChanged.length + batchSize - 1) / batchSize;\n\t\tlong batchesProcessed = 0;\n\t\t\n\t\t// For each batch of files to upload, upload the changed data to OneDrive\n\t\tforeach (chunk; databaseItemsWhereContentHasChanged.chunks(batchSize)) {\n\t\t\tprocessChangedLocalItemsToUploadInParallel(chunk);\n\t\t}\n\t\t\n\t\t// For this set of items, perform a DB PASSIVE checkpoint\n\t\titemDB.performCheckpoint(\"PASSIVE\");\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\n\t// Process all the changed local items in parallel\n\tvoid processChangedLocalItemsToUploadInParallel(string[3][] array) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\n\t\t// This function received an array of string items to upload, the number of elements based on appConfig.getValueLong(\"threads\")\n\t\tforeach (i, localItemDetails; processPool.parallel(array)) {\n\t\t\tif (debugLogging) {addLogEntry(\"Upload Thread \" ~ to!string(i) ~ \" Starting: \" ~ to!string(Clock.currTime()), [\"debug\"]);}\n\t\t\tuploadChangedLocalFileToOneDrive(localItemDetails);\n\t\t\tif (debugLogging) {addLogEntry(\"Upload Thread \" ~ to!string(i) ~ \" Finished: \" ~ to!string(Clock.currTime()), [\"debug\"]);}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Upload changed local files to OneDrive in parallel\n\tvoid uploadChangedLocalFileToOneDrive(string[3] localItemDetails) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// These are the details of the item we need to upload\n\t\tstring changedItemDriveId = localItemDetails[0];\n\t\tstring changedItemId = localItemDetails[1];\n\t\tstring localFilePath = localItemDetails[2];\n\t\t\n\t\t// Log the path that was modified\n\t\tif (debugLogging) {addLogEntry(\"uploadChangedLocalFileToOneDrive: \" ~ localFilePath, [\"debug\"]);}\n\t\t\n\t\t// How much space is remaining on OneDrive\n\t\tlong remainingFreeSpace;\n\t\t// Did the upload fail?\n\t\tbool uploadFailed = false;\n\t\t// Did we skip due to exceeding maximum allowed size?\n\t\tbool skippedMaxSize = false;\n\t\t// Did we skip to an exception error?\n\t\tbool skippedExceptionError = false;\n\t\t// Flag for if space is available online\n\t\tbool spaceAvailableOnline = false;\n\t\t\n\t\t// Capture what time this upload started\n\t\tSysTime uploadStartTime = Clock.currTime();\n\t\t\n\t\t// When we are uploading OneDrive Business Shared Files, we need to be targeting the right driveId and itemId\n\t\tstring targetDriveId;\n\t\tstring targetItemId;\n\t\t\n\t\t// Unfortunately, we cant store an array of Item's ... so we have to re-query the DB again - unavoidable extra processing here\n\t\t// 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\n\t\tItem dbItem;\n\t\titemDB.selectById(changedItemDriveId, changedItemId, dbItem);\n\t\t\n\t\t// Was a valid DB response returned\n\t\tif (!dbItem.driveId.empty) {\n\t\t\t// Is this a remote driveId target based on the database response?\n\t\t\tif ((dbItem.type == ItemType.remote) && (dbItem.remoteType == ItemType.file)) {\n\t\t\t\t// This is a remote file\n\t\t\t\ttargetDriveId = dbItem.remoteDriveId;\n\t\t\t\ttargetItemId = dbItem.remoteId;\n\t\t\t\t// we are going to make the assumption here that as this is a OneDrive Business Shared File, that there is space available\n\t\t\t\tspaceAvailableOnline = true;\n\t\t\t} else {\n\t\t\t\t// This is not a remote file\n\t\t\t\ttargetDriveId = dbItem.driveId;\n\t\t\t\ttargetItemId = dbItem.id;\n\t\t\t}\n\t\t} else {\n\t\t\t// No valid DB response was provided\n\t\t\tif (debugLogging) {\n\t\t\t\tstring logMessage = format(\"No valid DB response was provided when searching for '%s' and '%s'\", changedItemDriveId, changedItemId);\n\t\t\t\taddLogEntry(logMessage, [\"debug\"]);\n\t\t\t\t\n\t\t\t\t// Fetch the online data again for this file \n\t\t\t\taddLogEntry(\"Fetching latest online details for this item due to zero DB data available\", [\"debug\"]);\n\t\t\t}\n\t\t\t\t\n\t\t\tOneDriveApi checkFileOneDriveApiInstance;\n\t\t\tJSONValue fileDetailsFromOneDrive;\n\t\t\t\n\t\t\t// Create a new API Instance for this thread and initialise it\n\t\t\tcheckFileOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t\tcheckFileOneDriveApiInstance.initialise();\n\n\t\t\t// Try and get the absolute latest object details from online to potentially build a DB record we can use\n\t\t\ttry {\n\t\t\t\tfileDetailsFromOneDrive = checkFileOneDriveApiInstance.getPathDetailsById(changedItemDriveId, changedItemId);\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// Display what the error is\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tcheckFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\tcheckFileOneDriveApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t\t// Turn 'fileDetailsFromOneDrive' into a DB item\n\t\t\tif (fileDetailsFromOneDrive.type() == JSONType.object) {\n\t\t\t\t// Yes\n\t\t\t\tif (debugLogging) {addLogEntry(\"Creating DB item from online API response: \" ~ to!string(fileDetailsFromOneDrive), [\"debug\"]);}\n\t\t\t\tdbItem = makeItem(fileDetailsFromOneDrive);\n\t\t\t} else {\n\t\t\t\t// No\n\t\t\t\taddLogEntry(\"Unable to upload this modified file at this point in time: \" ~ localFilePath);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Are we in an --upload-only & --remove-source-files scenario?\n\t\t// - In this scenario, and even more so in a --resync scenario when using these options, there is potentially 100% zero database entry for the modified file we are uploading\n\t\t//   This will be in the logs when we are in this scenario:\n\t\t//     Skipping adding to database as --upload-only & --remove-source-files configured\n\t\tif ((uploadOnly) && (localDeleteAfterUpload)) {\n\t\t\t// We are in the potential scenario where 'targetDriveId' and 'targetItemId' are still an empty value(s)\n\t\t\t// Check targetDriveId\n\t\t\tif (targetDriveId.empty) {\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\tstring logMessage = format(\"Updating 'targetDriveId' to '%s' due to --upload-only and --remove-source-files being used\", changedItemDriveId);\n\t\t\t\t\taddLogEntry(logMessage, [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t// set the value\n\t\t\t\ttargetDriveId = changedItemDriveId;\n\t\t\t}\n\t\t\t// Check targetItemId\t\n\t\t\tif (targetItemId.empty) {\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\tstring logMessage = format(\"Updating 'targetItemId' to '%s' due to --upload-only and --remove-source-files being used\", changedItemId);\n\t\t\t\t\taddLogEntry(logMessage, [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t// set the value\n\t\t\t\ttargetItemId = changedItemId;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Fetch the details from cachedOnlineDriveData if this is available\n\t\t// - cachedOnlineDriveData.quotaRestricted;\n\t\t// - cachedOnlineDriveData.quotaAvailable;\n\t\t// - cachedOnlineDriveData.quotaRemaining;\n\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\t\n\t\t// Make sure that parentItem.driveId is in our driveIDs array to use when checking if item is in database\n\t\t// Keep the DriveDetailsCache array with unique entries only\n\t\tif (!canFindDriveId(targetDriveId, cachedOnlineDriveData)) {\n\t\t\t// Add this driveId to the drive cache, which then also sets for the defaultDriveId:\n\t\t\t// - quotaRestricted;\n\t\t\t// - quotaAvailable;\n\t\t\t// - quotaRemaining;\n\t\t\taddOrUpdateOneDriveOnlineDetails(targetDriveId);\n\t\t} \n\t\t\n\t\t// Query the details using the correct 'targetDriveId' for this modified file to be uploaded\n\t\tcachedOnlineDriveData = getDriveDetails(targetDriveId);\n\t\t\n\t\t// Configure 'remainingFreeSpace' based on the 'targetDriveId'\n\t\tremainingFreeSpace = cachedOnlineDriveData.quotaRemaining;\n\t\t\n\t\t// Get the file size from the actual file\n\t\tlong thisFileSizeLocal = getSize(localFilePath);\n\t\t\n\t\t// Get the file size from the DB data, if DB data was returned, otherwise we have zero size value from the DB\n\t\tlong thisFileSizeFromDB;\n\t\tif (!dbItem.size.empty) {\n\t\t\tthisFileSizeFromDB = to!long(dbItem.size);\n\t\t} else {\n\t\t\tthisFileSizeFromDB = 0;\n\t\t}\n\t\t\n\t\t// 'remainingFreeSpace' online includes the current file online\n\t\t// 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\n\t\tlong calculatedSpaceOnlinePostUpload = (remainingFreeSpace + thisFileSizeFromDB) - thisFileSizeLocal;\n\t\t\n\t\t// Based on what we know, for this thread - can we safely upload this modified local file?\n\t\tif (debugLogging) {\n\t\t\tstring estimatedMessage = format(\"This Thread (Upload Changed File) Estimated Free Space Online (%s): \", targetDriveId);\n\t\t\taddLogEntry(estimatedMessage ~ to!string(remainingFreeSpace), [\"debug\"]);\n\t\t\taddLogEntry(\"This Thread (Upload Changed File) Calculated Free Space Online Post Upload: \" ~ to!string(calculatedSpaceOnlinePostUpload), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Is there quota available for the given drive where we are uploading to?\n\t\t// \tIf 'personal' accounts, if driveId == defaultDriveId, then we will have quota data - cachedOnlineDriveData.quotaRemaining will be updated so it can be reused\n\t\t// \tIf 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data - cachedOnlineDriveData.quotaRestricted will be set as true\n\t\t// \tIf 'business' accounts, if driveId == defaultDriveId, then we will potentially have quota data - cachedOnlineDriveData.quotaRemaining will be updated so it can be reused\n\t\t// \tIf '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\n\t\tif (cachedOnlineDriveData.quotaAvailable) {\n\t\t\t// 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?\n\t\t\tif (calculatedSpaceOnlinePostUpload > 0) {\n\t\t\t\t// Based on this thread action, we believe that there is space available online to upload - proceed\n\t\t\t\tspaceAvailableOnline = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Is quota being restricted?\n\t\tif (cachedOnlineDriveData.quotaRestricted) {\n\t\t\t// Space available online is being restricted - so we have no way to really know if there is space available online\n\t\t\tspaceAvailableOnline = true;\n\t\t}\n\t\t\t\n\t\t// Do we have space available or is space available being restricted (so we make the blind assumption that there is space available)\n\t\tJSONValue uploadResponse;\n\t\tif (spaceAvailableOnline) {\n\t\t\t// Does this file exceed the maximum file size to upload to OneDrive?\n\t\t\tif (thisFileSizeLocal <= maxUploadFileSize) {\n\t\t\t\t// Attempt to upload the modified file\n\t\t\t\t// 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\n\t\t\t\tuploadResponse = performModifiedFileUpload(dbItem, localFilePath, thisFileSizeLocal);\n\t\t\t\t\n\t\t\t\t// Evaluate the returned JSON uploadResponse\n\t\t\t\t// If there was an error uploading the file, uploadResponse should be empty and invalid\n\t\t\t\tif (uploadResponse.type() != JSONType.object) {\n\t\t\t\t\tuploadFailed = true;\n\t\t\t\t\tskippedExceptionError = true;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t// Skip file - too large\n\t\t\t\tuploadFailed = true;\n\t\t\t\tskippedMaxSize = true;\n\t\t\t}\n\t\t} else {\n\t\t\t// Cant upload this file - no space available\n\t\t\tuploadFailed = true;\n\t\t}\n\t\t\n\t\t// Did the upload fail?\n\t\tif (uploadFailed) {\n\t\t\t// Upload failed .. why?\n\t\t\t// No space available online\n\t\t\tif (!spaceAvailableOnline) {\n\t\t\t\taddLogEntry(\"Skipping uploading modified file: \" ~ localFilePath ~ \" due to insufficient free space available on Microsoft OneDrive\", [\"info\", \"notify\"]);\n\t\t\t}\n\t\t\t// File exceeds max allowed size\n\t\t\tif (skippedMaxSize) {\n\t\t\t\taddLogEntry(\"Skipping uploading this modified file as it exceeds the maximum size allowed by Microsoft OneDrive: \" ~ localFilePath, [\"info\", \"notify\"]);\n\t\t\t}\n\t\t\t// Generic message\n\t\t\tif (skippedExceptionError) {\n\t\t\t\t// normal failure message if API or exception error generated\n\t\t\t\t// If Issue #2626 | Case 2-1 is triggered, the file we tried to upload was renamed, then uploaded as a new name\n\t\t\t\tif (exists(localFilePath)) {\n\t\t\t\t\t// Issue #2626 | Case 2-1 was not triggered, file still exists on local filesystem\n\t\t\t\t\taddLogEntry(\"Uploading modified file: \" ~ localFilePath ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Upload was successful\n\t\t\taddLogEntry(\"Uploading modified file: \" ~ localFilePath ~ \" ... done\", fileTransferNotifications());\n\t\t\t\n\t\t\t// As no upload failure, calculate transfer metrics in a consistent manner\n\t\t\tdisplayTransferMetrics(localFilePath, thisFileSizeLocal, uploadStartTime, Clock.currTime());\n\t\t\t\n\t\t\t// What do we save to the DB? Is this a OneDrive Business Shared File?\n\t\t\tif ((dbItem.type == ItemType.remote) && (dbItem.remoteType == ItemType.file)) {\n\t\t\t\t// We need to 'massage' the old DB record, with data from online, as the DB record was specifically crafted for OneDrive Business Shared Files\n\t\t\t\tItem tempItem = makeItem(uploadResponse);\n\t\t\t\tdbItem.eTag = tempItem.eTag;\n\t\t\t\tdbItem.cTag = tempItem.cTag;\n\t\t\t\tdbItem.mtime = tempItem.mtime;\n\t\t\t\tdbItem.quickXorHash = tempItem.quickXorHash;\n\t\t\t\tdbItem.sha256Hash = tempItem.sha256Hash;\n\t\t\t\tdbItem.size = tempItem.size;\n\t\t\t\titemDB.upsert(dbItem);\n\t\t\t} else {\n\t\t\t\t// Save the response JSON item in database as is\n\t\t\t\tsaveItem(uploadResponse);\n\t\t\t}\n\t\t\t\n\t\t\t// Update the 'cachedOnlineDriveData' record for this 'targetDriveId' so that this is tracked as accurately as possible for other threads\n\t\t\tupdateDriveDetailsCache(targetDriveId, cachedOnlineDriveData.quotaRestricted, cachedOnlineDriveData.quotaAvailable, thisFileSizeLocal);\n\t\t\t\n\t\t\t// Check the integrity of the uploaded modified file if not in a --dry-run scenario\n\t\t\tif (!dryRun) {\n\t\t\t\tbool uploadIntegrityPassed;\n\t\t\t\t// Check the integrity of the uploaded modified file, if the local file still exists\n\t\t\t\tuploadIntegrityPassed = performUploadIntegrityValidationChecks(uploadResponse, localFilePath, thisFileSizeLocal);\n\t\t\t\t\n\t\t\t\t// Update the date / time of the file online to match the local item\n\t\t\t\t// Get the local file last modified time\n\t\t\t\tSysTime localModifiedTime = timeLastModified(localFilePath).toUTC();\n\t\t\t\t// Drop fractional seconds for upload timestamp modification as Microsoft OneDrive does not support fractional seconds\n\t\t\t\tlocalModifiedTime.fracSecs = Duration.zero;\n\t\t\t\t\n\t\t\t\t// Get the latest eTag, and use that\n\t\t\t\tstring etagFromUploadResponse = uploadResponse[\"eTag\"].str;\n\t\t\t\t\n\t\t\t\t// Attempt to update the online lastModifiedDateTime value based on our local timestamp data\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t// Personal Account Handling for Modified File Upload\n\t\t\t\t\t//\n\t\t\t\t\t// Did the upload integrity check pass or fail?\n\t\t\t\t\tif (!uploadIntegrityPassed) {\n\t\t\t\t\t\t// upload integrity check failed for the modified file\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"create_new_file_version\")) {\n\t\t\t\t\t\t\t// warn that file differences will exist online\n\t\t\t\t\t\t\t// as this is a 'personal' account .. we have no idea / reason potentially, so do not download the 'online' file\n\t\t\t\t\t\t\taddLogEntry(\"WARNING: The file uploaded to Microsoft OneDrive does not match your local version. Data loss may occur.\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Create a new online version of the file by updating the online metadata\n\t\t\t\t\t\t\tuploadLastModifiedTime(dbItem, targetDriveId, targetItemId, localModifiedTime, etagFromUploadResponse);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Upload of the modified file passed integrity checks\n\t\t\t\t\t\t// We need to make sure that the local file on disk has this timestamp from this JSON, otherwise on the next application run:\n\t\t\t\t\t\t//   The last modified timestamp has changed however the file content has not changed\n\t\t\t\t\t\t//   The local item has the same hash value as the item online - correcting timestamp online\n\t\t\t\t\t\t// This then creates another version online which we do not want to do .. unless configured to do so\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"create_new_file_version\")) {\n\t\t\t\t\t\t\t// Are we in an --upload-only scenario?\n\t\t\t\t\t\t\t// In in an --upload-only scenario, it is pointless updating the local timestamp with that what is now online\n\t\t\t\t\t\t\tif(!uploadOnly){\n\t\t\t\t\t\t\t\t// Create an applicable DB item from the upload JSON response\n\t\t\t\t\t\t\t\tItem onlineItem;\n\t\t\t\t\t\t\t\tonlineItem = makeItem(uploadResponse);\n\t\t\t\t\t\t\t\t// Correct the local file timestamp to avoid creating a new version online\n\t\t\t\t\t\t\t\t// Set the local timestamp, logging and error handling done within function\n\t\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, localFilePath, onlineItem.mtime);\n\t\t\t\t\t\t\t}\t\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Create a new online version of the file by updating the metadata, which negates the need to download the file\n\t\t\t\t\t\t\tuploadLastModifiedTime(dbItem, targetDriveId, targetItemId, localModifiedTime, etagFromUploadResponse);\t\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Business | SharePoint Account Handling for Modified File Upload\n\t\t\t\t\t//\n\t\t\t\t\t// 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.\n\t\t\t\t\t// This means that the file which was uploaded, is potentially no longer the file we have locally\n\t\t\t\t\t// There are 2 ways to solve this:\n\t\t\t\t\t//   1. Download the modified file immediately after upload as per v2.4.x (default)\n\t\t\t\t\t//   2. Create a new online version of the file, which then contributes to the users 'quota'\n\t\t\t\t\t// Did the upload integrity check pass or fail?\n\t\t\t\t\tif (!uploadIntegrityPassed) {\n\t\t\t\t\t\t// upload integrity check failed for the modified file\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"create_new_file_version\")) {\n\t\t\t\t\t\t\t// Are we in an --upload-only scenario?\n\t\t\t\t\t\t\tif(!uploadOnly){\n\t\t\t\t\t\t\t\t// Download the now online modified file\n\t\t\t\t\t\t\t\taddLogEntry(\"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.\");\n\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details.\");\n\t\t\t\t\t\t\t\t// Download the file directly using the prior upload JSON response\n\t\t\t\t\t\t\t\tdownloadFileItem(uploadResponse, true);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// --upload-only being used\n\t\t\t\t\t\t\t\t// we are not downloading a file, warn that file differences will exist\n\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: The file uploaded to Microsoft OneDrive has been modified through its SharePoint 'enrichment' process and no longer matches your local version.\");\n\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Create a new online version of the file by updating the metadata, which negates the need to download the file\n\t\t\t\t\t\t\tuploadLastModifiedTime(dbItem, targetDriveId, targetItemId, localModifiedTime, etagFromUploadResponse);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Upload of the modified file passed integrity checks\n\t\t\t\t\t\t// We need to make sure that the local file on disk has this timestamp from this JSON, otherwise on the next application run:\n\t\t\t\t\t\t//   The last modified timestamp has changed however the file content has not changed\n\t\t\t\t\t\t//   The local item has the same hash value as the item online - correcting timestamp online\n\t\t\t\t\t\t// This then creates another version online which we do not want to do .. unless configured to do so\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"create_new_file_version\")) {\n\t\t\t\t\t\t\t// Are we in an --upload-only scenario?\n\t\t\t\t\t\t\t// In in an --upload-only scenario, it is pointless updating the local timestamp with that what is now online\n\t\t\t\t\t\t\tif(!uploadOnly){\n\t\t\t\t\t\t\t\t// Create an applicable DB item from the upload JSON response\n\t\t\t\t\t\t\t\tItem onlineItem;\n\t\t\t\t\t\t\t\tonlineItem = makeItem(uploadResponse);\n\t\t\t\t\t\t\t\t// Correct the local file timestamp to avoid creating a new version online\n\t\t\t\t\t\t\t\t// Set the timestamp, logging and error handling done within function\n\t\t\t\t\t\t\t\tsetLocalPathTimestamp(dryRun, localFilePath, onlineItem.mtime);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Create a new online version of the file by updating the metadata, which negates the need to download the file\n\t\t\t\t\t\t\tuploadLastModifiedTime(dbItem, targetDriveId, targetItemId, localModifiedTime, etagFromUploadResponse);\t\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Are we in an --upload-only & --remove-source-files scenario?\n\t\t\t\tif ((uploadOnly) && (localDeleteAfterUpload)) {\n\t\t\t\t\t// Perform the local file deletion\n\t\t\t\t\tremoveLocalFilePostUpload(localFilePath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Remove the local file if using --upload-only & --remove-source-files scenario in a consistent manner\n\tvoid removeLocalFilePostUpload(string localPathToRemove) {\n\t\t// File has to exist before removal\n\t\tif (exists(localPathToRemove)) {\n\t\t\t// Log that we are deleting a local item\n\t\t\taddLogEntry(\"Attempting removal of local file as --upload-only & --remove-source-files configured\");\n\t\t\t\n\t\t\t// Are we in a --dry-run scenario?\n\t\t\tif (!dryRun) {\n\t\t\t\t// Not in a --dry-run scenario\n\t\t\t\tif (debugLogging) {addLogEntry(\"Removing local file: \" ~ localPathToRemove, [\"debug\"]);}\n\t\t\t\tsafeRemove(localPathToRemove);\n\t\t\t\taddLogEntry(\"Removed local file:  \" ~ localPathToRemove);\n\t\t\t\t\n\t\t\t\t// Do we try and attempt to remove the local source tree?\n\t\t\t\tif (appConfig.getValueBool(\"remove_source_folders\")) {\n\t\t\t\t\t// Remove the source directory structure but only if it is empty\n\t\t\t\t\taddLogEntry(\"Attempting removal of local directory structure as --upload-only & --remove-source-files & --remove-source-folders configured\");\n\t\t\t\t\tstring parentPath = dirName(localPathToRemove);\n\t\t\t\t\tremoveEmptyParents(localPathToRemove);\n\t\t\t\t\taddLogEntry(\"Removed parental path:  \" ~ parentPath);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// --dry-run scenario\n\t\t\t\taddLogEntry(\"Not removing local file as --dry-run configured\");\n\t\t\t}\n\t\t} else {\n\t\t\t// Log that the path to remove does not exist locally\n\t\t\taddLogEntry(\"Removing local file not possible as local file does not exist\");\n\t\t}\n\t}\n\t\n\t// Remove empty parent directories of `filePath` upwards until:\n\t//  - we hit a non-empty directory, or\n\t//  - we reach the visible root (i.e. dirName(current) == \".\").\n\t// Never tries to remove \".\".\n\tvoid removeEmptyParents(string filePath) {\n\t\t// Work with a normalised *relative* path inside the chrooted configured 'sync_dir'\n\t\t// If someone passed an absolute path, normalise it anyway; your codebase\n\t\t// likely already ensures paths are relative within the sync root.\n\t\tstring current = dirName(buildNormalizedPath(filePath));\n\t\t\n\t\twhile (current.length && current != \".\") {\n\t\t\t// Safety: don’t descend into symlinks\n\t\t\tif (isSymlink(current)) {\n\t\t\t\tif (debugLogging) addLogEntry(\"Skipping removal; parent is a symlink: \" ~ current, [\"debug\"]);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Stop at first non-empty directory\n\t\t\tif (!isDirEmpty(current)) {\n\t\t\t\tif (debugLogging) addLogEntry(\"Stopping prune; directory not empty: \" ~ current, [\"debug\"]);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (!dryRun) {\n\t\t\t\tif (debugLogging) addLogEntry(\"Removing empty directory: \" ~ current, [\"debug\"]);\n\t\t\t\t// rmdir only succeeds for empty directories; errors are collected not thrown\n\t\t\t\tcollectException(rmdir(current));\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"Not removing empty directory as --dry-run configured: \" ~ current);\n\t\t\t}\n\n\t\t\t// Move up one level\n\t\t\tstring next = dirName(current);\n\t\t\tif (next == current) { // Just in case (shouldn’t happen with relative paths)\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcurrent = next;\n\t\t}\n\t}\n\t\t\t\n\t// Perform the upload of a locally modified file to OneDrive\n\tJSONValue performModifiedFileUpload(Item dbItem, string localFilePath, long thisFileSizeLocal) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\n\t\t// Function variables\n\t\tJSONValue uploadResponse;\n\t\tOneDriveApi uploadFileOneDriveApiInstance;\n\t\tuploadFileOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tuploadFileOneDriveApiInstance.initialise();\n\t\t\n\t\t// Configure JSONValue variables we use for a session upload\n\t\tJSONValue currentOnlineJSONData;\n\t\tItem currentOnlineItemData;\n\t\tJSONValue uploadSessionData;\n\t\tstring currentETag;\n\t\t\n\t\t// When we are uploading OneDrive Business Shared Files, we need to be targeting the right driveId and itemId\n\t\tstring targetDriveId;\n\t\tstring targetParentId;\n\t\tstring targetItemId;\n\t\t\n\t\t// Is this a remote target?\n\t\tif ((dbItem.type == ItemType.remote) && (dbItem.remoteType == ItemType.file)) {\n\t\t\t// This is a remote file\n\t\t\ttargetDriveId = dbItem.remoteDriveId;\n\t\t\ttargetParentId = dbItem.remoteParentId;\n\t\t\ttargetItemId = dbItem.remoteId;\n\t\t} else {\n\t\t\t// This is not a remote file\n\t\t\ttargetDriveId = dbItem.driveId;\n\t\t\ttargetParentId = dbItem.parentId;\n\t\t\ttargetItemId = dbItem.id;\n\t\t}\n\t\t\n\t\t// Is this a dry-run scenario?\n\t\tif (!dryRun) {\n\t\t\t// Do we use simpleUpload or create an upload session?\n\t\t\tbool useSimpleUpload = false;\n\t\t\t\n\t\t\t// Try and get the absolute latest object details from online, so we get the latest eTag to try and avoid a 412 eTag error\n\t\t\ttry {\n\t\t\t\tcurrentOnlineJSONData = uploadFileOneDriveApiInstance.getPathDetailsById(targetDriveId, targetItemId);\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// Display what the error is\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t\t\n\t\t\t// Was a valid JSON response provided?\n\t\t\tif (currentOnlineJSONData.type() == JSONType.object) {\n\t\t\t\t// Does the response contain an eTag?\n\t\t\t\tif (hasETag(currentOnlineJSONData)) {\n\t\t\t\t\t// Use the value returned from online as this will attempt to avoid a 412 response if we are creating a session upload\n\t\t\t\t\tcurrentETag = currentOnlineJSONData[\"eTag\"].str;\n\t\t\t\t} else {\n\t\t\t\t\t// Use the database value - greater potential for a 412 error to occur if we are creating a session upload\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Online data for file returned zero eTag - using database eTag value\", [\"debug\"]);}\n\t\t\t\t\tcurrentETag = dbItem.eTag;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Make a reusable item from this online JSON data\n\t\t\t\tcurrentOnlineItemData = makeItem(currentOnlineJSONData);\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t// no valid JSON response - greater potential for a 412 error to occur if we are creating a session upload\n\t\t\t\tif (debugLogging) {addLogEntry(\"Online data returned was invalid - using database eTag value\", [\"debug\"]);}\n\t\t\t\tcurrentETag = dbItem.eTag;\n\t\t\t}\n\t\t\t\n\t\t\t// What upload method should be used?\n\t\t\tif (thisFileSizeLocal <= sessionThresholdFileSize) {\n\t\t\t\t// file size is below session threshold\n\t\t\t\tuseSimpleUpload = true;\n\t\t\t}\n\t\t\t\n\t\t\t// Use Session Upload regardless\n\t\t\tif (appConfig.getValueBool(\"force_session_upload\")) {\n\t\t\t\t// Forcing session upload\n\t\t\t\tif (debugLogging) {addLogEntry(\"Forcing to perform upload using a session (modified)\", [\"debug\"]);}\n\t\t\t\tuseSimpleUpload = false;\n\t\t\t}\n\t\t\t\n\t\t\t// 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?\n\t\t\tif ((thisFileSizeLocal > 0) && (currentOnlineJSONData.type() == JSONType.object)) {\n\t\t\t\t// Issue #2626 | Case 2-1 \n\t\t\t\t// If the 'online' file is newer, this will be overwritten with the file from the local filesystem - potentially constituting online data loss\n\t\t\t\tItem onlineFile = makeItem(currentOnlineJSONData);\n\t\t\t\t\n\t\t\t\t// Which file is technically newer? The local file or the remote file?\n\t\t\t\tSysTime localModifiedTime = timeLastModified(localFilePath).toUTC();\n\t\t\t\tSysTime onlineModifiedTime = onlineFile.mtime;\n\t\t\t\t\n\t\t\t\t// Reduce time resolution to seconds before comparing\n\t\t\t\tlocalModifiedTime.fracSecs = Duration.zero;\n\t\t\t\tonlineModifiedTime.fracSecs = Duration.zero;\n\t\t\t\t\n\t\t\t\t// Which file is newer? If local is newer, it will be uploaded as a modified file in the correct manner\n\t\t\t\tif (localModifiedTime < onlineModifiedTime) {\n\t\t\t\t\t// Online File is actually newer than the locally modified file\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"currentOnlineJSONData: \" ~ to!string(currentOnlineJSONData), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"onlineFile:    \" ~ to!string(onlineFile), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"database item: \" ~ to!string(dbItem), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\taddLogEntry(\"Skipping uploading this item as a locally modified file, will upload as a new file (online file already exists and is newer): \" ~ localFilePath);\n\t\t\t\t\t\n\t\t\t\t\t// Online is newer, rename local, then upload the renamed file\n\t\t\t\t\t// We need to know the renamed path so we can upload it\n\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t// Rename the local path - we WANT this to occur regardless of bypassDataPreservation setting\n\t\t\t\t\tsafeBackup(localFilePath, dryRun, false, renamedPath);\n\t\t\t\t\t// Upload renamed local file as a new file\n\t\t\t\t\tuploadNewFile(renamedPath);\n\t\t\t\t\t\n\t\t\t\t\t// Process the database entry removal for the original file. In a --dry-run scenario, this is being done against a DB copy.\n\t\t\t\t\t// This is done so we can download the newer online file\n\t\t\t\t\titemDB.deleteById(targetDriveId, targetItemId);\n\n\t\t\t\t\t// 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\n\t\t\t\t\treturn uploadResponse;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// We can only upload zero size files via simpleFileUpload regardless of account type\n\t\t\t// Reference: https://github.com/OneDrive/onedrive-api-docs/issues/53\n\t\t\t// Additionally, all files where file size is < 4MB should be uploaded by simpleUploadReplace - everything else should use a session to upload the modified file\n\t\t\tif ((thisFileSizeLocal == 0) || (useSimpleUpload)) {\n\t\t\t\t// Must use Simple Upload to replace the file online\n\t\t\t\ttry {\n\t\t\t\t\tuploadResponse = uploadFileOneDriveApiInstance.simpleUploadReplace(localFilePath, targetDriveId, targetItemId);\n\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t// HTTP request returned status code 403\n\t\t\t\t\tif ((exception.httpStatusCode == 403) && (appConfig.getValueBool(\"sync_business_shared_files\"))) {\n\t\t\t\t\t\t// We attempted to upload a file, that was shared with us, but this was shared with us as read-only\n\t\t\t\t\t\taddLogEntry(\"Unable to upload this modified file as this was shared as read-only: \" ~ localFilePath);\n\t\t\t\t\t}\n\t\t\t\t\t// HTTP request returned status code 423\n\t\t\t\t\t// Resolve https://github.com/abraunegg/onedrive/issues/36\n\t\t\t\t\tif (exception.httpStatusCode == 423) {\n\t\t\t\t\t\t// The file is currently checked out or locked for editing by another user\n\t\t\t\t\t\t// We cant upload this file at this time\n\t\t\t\t\t\taddLogEntry(\"Unable to upload this modified file as this is currently checked out or locked for editing by another user: \" ~ localFilePath);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Handle all other HTTP status codes\n\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t}\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// filesystem error\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, localFilePath);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// As this is a unique thread, the sessionFilePath for where we save the data needs to be unique\n\t\t\t\t// The best way to do this is generate a 10 digit alphanumeric string, and use this as the file extension\n\t\t\t\tstring threadUploadSessionFilePath = appConfig.uploadSessionFilePath ~ \".\" ~ generateAlphanumericString();\n\t\t\t\t\n\t\t\t\t// Create the upload session using the latest online data 'currentOnlineData' etag\n\t\t\t\ttry {\n\t\t\t\t\t// create the session\n\t\t\t\t\tuploadSessionData = createSessionForFileUpload(uploadFileOneDriveApiInstance, localFilePath, targetDriveId, targetParentId, baseName(localFilePath), currentOnlineItemData.eTag, threadUploadSessionFilePath);\n\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t// HTTP request returned status code 403\n\t\t\t\t\tif ((exception.httpStatusCode == 403) && (appConfig.getValueBool(\"sync_business_shared_files\"))) {\n\t\t\t\t\t\t// We attempted to upload a file, that was shared with us, but this was shared with us as read-only\n\t\t\t\t\t\taddLogEntry(\"Unable to upload this modified file as this was shared as read-only: \" ~ localFilePath);\n\t\t\t\t\t\treturn uploadResponse;\n\t\t\t\t\t} \n\t\t\t\t\t\n\t\t\t\t\t// HTTP request returned status code 423\n\t\t\t\t\t// Resolve https://github.com/abraunegg/onedrive/issues/36\n\t\t\t\t\tif (exception.httpStatusCode == 423) {\n\t\t\t\t\t\t// The file is currently checked out or locked for editing by another user\n\t\t\t\t\t\t// We cant upload this file at this time\n\t\t\t\t\t\taddLogEntry(\"Unable to upload this modified file as this is currently checked out or locked for editing by another user: \" ~ localFilePath);\n\t\t\t\t\t\treturn uploadResponse;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Handle all other HTTP status codes\n\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t}\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// Display filesystem exception error message\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, threadUploadSessionFilePath);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Do we have a valid session URL that we can use ?\n\t\t\t\tif (uploadSessionData.type() == JSONType.object) {\n\t\t\t\t\t// This is a valid JSON object\n\t\t\t\t\t// Perform the upload using the session that has been created\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// so that we have this data available if we need to re-create the session\n\t\t\t\t\t\t// - targetDriveId, targetParentId, baseName(localFilePath), currentOnlineItemData.eTag, threadUploadSessionFilePath\n\t\t\t\t\t\tuploadSessionData[\"targetDriveId\"] = targetDriveId;\n\t\t\t\t\t\tuploadSessionData[\"targetParentId\"] = targetParentId;\n\t\t\t\t\t\tuploadSessionData[\"currentETag\"] = currentOnlineItemData.eTag;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// attempt the session upload using the session data provided\n\t\t\t\t\t\tuploadResponse = performSessionFileUpload(uploadFileOneDriveApiInstance, thisFileSizeLocal, uploadSessionData, threadUploadSessionFilePath);\n\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\t// Handle all other HTTP status codes\n\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t\n\t\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t\t// Display filesystem exception error message\n\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, threadUploadSessionFilePath);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Create session Upload URL failed\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Unable to upload modified file as the creation of the upload session URL failed\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// We are in a --dry-run scenario\n\t\t\tuploadResponse = createFakeResponse(localFilePath);\n\t\t}\n\t\t\n\t\t// Debug Log the modified upload response\n\t\tif (debugLogging) {addLogEntry(\"Modified File Upload Response: \" ~ to!string(uploadResponse), [\"debug\"]);}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tuploadFileOneDriveApiInstance.releaseCurlEngine();\n\t\tuploadFileOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return JSON\n\t\treturn uploadResponse;\n\t}\n\t\t\n\t// Query the OneDrive API using the provided driveId to get the latest quota details\n\tstring[3][] getRemainingFreeSpaceOnline(string sourceDriveId) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Get the quota details for this sourceDriveId\n\t\t// Quota details are ONLY available for the main default sourceDriveId, as the OneDrive API does not provide quota details for shared folders\n\t\tJSONValue currentDriveQuota;\n\t\tbool quotaRestricted = false; // Assume quota is not restricted unless \"remaining\" is missing\n\t\tbool quotaAvailable = false;\n\t\tlong quotaRemainingOnline = 0;\n\t\tstring[3][] result;\n\t\tOneDriveApi getCurrentDriveQuotaApiInstance;\n\t\tstring driveId;\n\n\t\t// Issue #3115 - Validate sourceDriveId length\n\t\t// What account type is this?\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Test sourceDriveId length and validation\n\t\t\tif (!sourceDriveId.empty) {\n\t\t\t\t// We were provided a sourceDriveId - that is what we check\n\t\t\t\tdriveId = transformToLowerCase(testProvidedDriveIdForLengthIssue(sourceDriveId));\n\t\t\t} else {\n\t\t\t\t// No sourceDriveId provided - use appConfig.defaultDriveId and validate that\n\t\t\t\tdriveId = transformToLowerCase(testProvidedDriveIdForLengthIssue(appConfig.defaultDriveId));\n\t\t\t}\n\t\t} else {\n\t\t\t// This is not a personal account type\n\t\t\t// Ensure that we have a valid driveId to query\n\t\t\tif (sourceDriveId.empty) {\n\t\t\t\t// No 'driveId' was provided, use the application default\n\t\t\t\tdriveId = appConfig.defaultDriveId;\n\t\t\t} else {\n\t\t\t\t// A 'driveId' was provided, use the provided 'sourceDriveId'\n\t\t\t\tdriveId = sourceDriveId;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Try and query the quota for the provided driveId\n\t\ttry {\n\t\t\t// Create a new OneDrive API instance\n\t\t\tgetCurrentDriveQuotaApiInstance = new OneDriveApi(appConfig);\n\t\t\tgetCurrentDriveQuotaApiInstance.initialise();\n\t\t\tif (debugLogging) {addLogEntry(\"Seeking available quota for this drive id: \" ~ driveId, [\"debug\"]);}\n\t\t\tcurrentDriveQuota = getCurrentDriveQuotaApiInstance.getDriveQuota(driveId);\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tgetCurrentDriveQuotaApiInstance.releaseCurlEngine();\n\t\t\tgetCurrentDriveQuotaApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t} catch (OneDriveException e) {\n\t\t\tif (debugLogging) {addLogEntry(\"currentDriveQuota = onedrive.getDriveQuota(driveId) generated a OneDriveException\", [\"debug\"]);}\n\t\t\t// If an exception occurs, it's unclear if quota is restricted, but quota details are not available\n\t\t\tquotaRestricted = true; // Considering restricted due to failure to access\n\t\t\t// Return result\n\t\t\tresult ~= [to!string(quotaRestricted), to!string(quotaAvailable), to!string(quotaRemainingOnline)];\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tgetCurrentDriveQuotaApiInstance.releaseCurlEngine();\n\t\t\tgetCurrentDriveQuotaApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\treturn result;\n\t\t}\n\t\t\n\t\t// Validate that currentDriveQuota is a JSON value\n\t\tif (currentDriveQuota.type() == JSONType.object && \"quota\" in currentDriveQuota) {\n\t\t\t// Response from API contains valid data\n\t\t\t// If 'personal' accounts, if driveId == defaultDriveId, then we will have data\n\t\t\t// If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data\n\t\t\t// If 'business' accounts, if driveId == defaultDriveId, then we will have data\n\t\t\t// If 'business' accounts, if driveId != defaultDriveId, then we will have data, but it will be a 0 value\n\t\t\tJSONValue quota = currentDriveQuota[\"quota\"];\n\t\t\t\n\t\t\t// debug output the entire 'quota' JSON response\n\t\t\tif (debugLogging) {addLogEntry(\"Quota Details: \" ~ to!string(quota), [\"debug\"]);}\n\t\t\t\n\t\t\t// Does the 'quota' JSON struct contain 'remaining' ?\n\t\t\tif (\"remaining\" in quota) {\n\t\t\t\t// Issue #2806\n\t\t\t\t// If this is a negative value, quota[\"remaining\"].integer can potentially convert to a huge positive number. Convert a different way.\n\t\t\t\tstring tempQuotaRemainingOnlineString;\n\t\t\t\t// is quota[\"remaining\"] an integer type?\n\t\t\t\tif (quota[\"remaining\"].type() == JSONType.integer) {\n\t\t\t\t\t// debug logging of the 'remaining' JSON struct\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"quota remaining is an integer value - using this value: \" ~ to!string(quota[\"remaining\"].integer), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t\t// extract as integer and convert to string\n\t\t\t\t\ttempQuotaRemainingOnlineString = to!string(quota[\"remaining\"].integer);\n\t\t\t\t} \n\t\t\t\t\n\t\t\t\t// Is 'tempQuotaRemainingOnlineString' still empty post integer check?\n\t\t\t\tif (tempQuotaRemainingOnlineString.empty) {\n\t\t\t\t\t// debug log that 'tempQuotaRemainingOnlineString' is still empty post integer check\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"tempQuotaRemainingOnlineString is still empty post integer JSON value analysis ..\", [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\n\t\t\t\t\t// is quota[\"remaining\"] an string type?\n\t\t\t\t\tif (quota[\"remaining\"].type() == JSONType.string) {\n\t\t\t\t\t\t// debug logging of the 'remaining' JSON struct\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"quota remaining is an string value - using this value: \" ~ to!string(quota[\"remaining\"].str), [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t\t// extract JSON value as string\n\t\t\t\t\t\ttempQuotaRemainingOnlineString = quota[\"remaining\"].str;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Fallback if tempQuotaRemainingOnlineString is still empty \n\t\t\t\tif (tempQuotaRemainingOnlineString.empty) {\n\t\t\t\t\t// debug log that 'tempQuotaRemainingOnlineString' is still empty\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"tempQuotaRemainingOnlineString is still empty post integer and string JSON value analysis .. this means quota 'remaining' element was not a string or integer value\", [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Fetch the details from cachedOnlineDriveData\n\t\t\t\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\t\t\t\tcachedOnlineDriveData = getDriveDetails(appConfig.defaultDriveId);\n\t\t\t\t\n\t\t\t\t\t// Use cachedOnlineDriveData.quotaRemaining as this is the last value we potentially had\n\t\t\t\t\tif ((cachedOnlineDriveData.quotaRemaining) > 0) {\n\t\t\t\t\t\t// the last known quota remaining was above zero\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"cachedOnlineDriveData.quotaRemaining is a positive value, using this last known value for tempQuotaRemainingOnlineString\", [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// set tempQuotaRemainingOnlineString to cachedOnlineDriveData.quotaRemaining\n\t\t\t\t\t\ttempQuotaRemainingOnlineString = to!string(cachedOnlineDriveData.quotaRemaining);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"cachedOnlineDriveData.quotaRemaining is zero or negative value, setting tempQuotaRemainingOnlineString to zero\", [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// no option but to set to zero\n\t\t\t\t\t\ttempQuotaRemainingOnlineString = \"0\";\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// What did we set 'tempQuotaRemainingOnlineString' to?\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"tempQuotaRemainingOnlineString = \" ~ tempQuotaRemainingOnlineString, [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Update quotaRemainingOnline to use the converted string value\n\t\t\t\tquotaRemainingOnline = to!long(tempQuotaRemainingOnlineString);\n\t\t\t\t\n\t\t\t\t// What did we set 'quotaRemainingOnline' to?\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"quotaRemainingOnline = \" ~ to!string(quotaRemainingOnline), [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Set the applicable 'quotaAvailable' value\n\t\t\t\tquotaAvailable = quotaRemainingOnline > 0;\n\t\t\t\t\n\t\t\t\t// If \"remaining\" is present but its value is <= 0, it's not restricted but exhausted\n\t\t\t\tif (quotaRemainingOnline <= 0) {\n\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\taddLogEntry(\"ERROR: OneDrive account currently has zero space available. Please free up some space online or purchase additional capacity.\");\n\t\t\t\t\t} else { // Assuming 'business' or 'sharedLibrary'\n\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// \"remaining\" not present, indicating restricted quota information\n\t\t\t\tquotaRestricted = true;\n\t\t\t\t\n\t\t\t\t// what sort of account type is this?\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"ERROR: OneDrive quota information is missing. Your OneDrive account potentially has zero space available. Please free up some space online.\", [\"verbose\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// quota details not available\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"WARNING: OneDrive quota information is being restricted. Please fix by speaking to your OneDrive / Office 365 Administrator.\", [\"verbose\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// When valid quota details are not fetched\n\t\t\tif (verboseLogging) {addLogEntry(\"Failed to fetch or query quota details for OneDrive Drive ID: \" ~ driveId, [\"verbose\"]);}\n\t\t\tquotaRestricted = true; // Considering restricted due to failure to interpret\n\t\t}\n\n\t\t// What was the determined available quota?\n\t\tif (debugLogging) {addLogEntry(\"Reported Available Online Quota for driveID '\" ~ driveId ~ \"': \" ~ to!string(quotaRemainingOnline), [\"debug\"]);}\n\t\t\n\t\t// Return result\n\t\tresult ~= [to!string(quotaRestricted), to!string(quotaAvailable), to!string(quotaRemainingOnline)];\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return new drive array data\n\t\treturn result;\n\t}\n\n\t// Perform a filesystem walk to uncover new data to upload to OneDrive\n\tvoid scanLocalFilesystemPathForNewData(string path) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Cleanup array memory before we start adding files\n\t\tpathsToCreateOnline = [];\n\t\tnewLocalFilesToUploadToOneDrive = [];\n\t\t\n\t\t// Perform a filesystem walk to uncover new data\n\t\tscanLocalFilesystemPathForNewDataToUpload(path);\n\t\t\n\t\t// Create new directories online that has been identified\n\t\tprocessNewDirectoriesToCreateOnline();\n\t\t\n\t\t// Upload new data that has been identified\n\t\tprocessNewLocalItemsToUpload();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\n\t// Scan the local filesystem for new data to upload\n\tvoid scanLocalFilesystemPathForNewDataToUpload(string path) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// To improve logging output for this function, what is the 'logical path' we are scanning for file & folder differences?\n\t\tstring logPath;\n\t\tif (path == \".\") {\n\t\t\t// get the configured sync_dir\n\t\t\tlogPath = buildNormalizedPath(appConfig.getValueString(\"sync_dir\"));\n\t\t} else {\n\t\t\t// use what was passed in\n\t\t\tif (!appConfig.getValueBool(\"monitor\")) {\n\t\t\t\tlogPath = buildNormalizedPath(appConfig.getValueString(\"sync_dir\")) ~ \"/\" ~ path;\n\t\t\t} else {\n\t\t\t\tlogPath = path;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Log the action that we are performing, however only if this is a directory\n\t\tif (exists(path)) {\n\t\t\tif (isDir(path)) {\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\tif (!cleanupLocalFiles) {\n\t\t\t\t\t\taddProcessingLogHeaderEntry(\"Scanning the local file system '\" ~ logPath ~ \"' for new data to upload\", appConfig.verbosityCount);\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddProcessingLogHeaderEntry(\"Scanning the local file system '\" ~ logPath ~ \"' for data to cleanup\", appConfig.verbosityCount);\n\t\t\t\t\t\t// Set the cleanup flag\n\t\t\t\t\t\tcleanupDataPass = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\tSysTime startTime;\n\t\tif (debugLogging) {\n\t\t\tstartTime = Clock.currTime();\n\t\t\taddLogEntry(\"Starting Filesystem Walk (Local Time): \" ~ to!string(startTime), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Add a processing '.' if this is a directory we are scanning\n\t\tif (exists(path)) {\n\t\t\tif (isDir(path)) {\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t\t\taddProcessingDotEntry();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Perform the filesystem walk of this path, building an array of new items to upload\n\t\tscanPathForNewData(path);\n\t\t// Reset flag\n\t\tcleanupDataPass = false;\n\t\t\n\t\t// Close processing '.' if this is a directory we are scanning\n\t\tif (exists(path)) {\n\t\t\tif (isDir(path)) {\n\t\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\t\t// Close out the '....' being printed to the console\n\t\t\t\t\t\tcompleteProcessingDots();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// To finish off the processing items, this is needed to reflect this in the log\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(debugLogBreakType1, [\"debug\"]);\n\t\t\t// finish filesystem walk time\n\t\t\tSysTime finishTime = Clock.currTime();\n\t\t\taddLogEntry(\"Finished Filesystem Walk (Local Time): \" ~ to!string(finishTime), [\"debug\"]);\n\t\t\t// duration\n\t\t\tDuration elapsedTime = finishTime - startTime;\n\t\t\taddLogEntry(\"Elapsed Time Filesystem Walk:          \" ~ to!string(elapsedTime), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Ensure we have a full list of unique paths to create online\n\tvoid addPathToCreateOnline(string pathToAdd) {\n\t\n\t\t// Is this a valid path to add?\n\t\t// The requested directory to create was not found on OneDrive - creating remote directory: ./.\n\t\t// \t\tOneDrive generated an error when creating this path: ./.\n\t\t// \t\tERROR: Microsoft OneDrive API returned an error with the following message:\n\t\t// \t\t  Error Message:       HTTP request returned status code 400 (Bad Request)\n\t\t// \t\t  Error Reason:        Invalid request\n\t\t// \t\t  Error Code:          invalidRequest\n\t\t// \t\t  Error Timestamp:     2025-05-02T20:31:46\n\t\t// \t\t  API Request ID:      23c2e2cd-6968-4a99-ac80-f9da786a18fd\n\t\t// \t\t  Calling Function:    syncEngine.createDirectoryOnline()\n\n\t\t// Is this a valid path to add?\n\t\tif ((pathToAdd == \".\")||(pathToAdd == \"./.\")) {\n\t\t\t// matches paths we should not attempt to create online\n\t\t\tif (debugLogging) {addLogEntry(\"attempted to add as path to create online - rejecting: \" ~ pathToAdd, [\"debug\"]);}\n\t\t\n\t\t\t// We can never add or create online the OneDrive 'root'\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Only add unique paths\n\t\tif (!pathsToCreateOnline.canFind(pathToAdd)) {\n\t\t\t// Add this unique path to the created online\n\t\t\t\n\t\t\t// are we in a --dry-run scenario?\n\t\t\tif (!dryRun) {\n\t\t\t\t// Add this to the list to create online\n\t\t\t\tpathsToCreateOnline ~= pathToAdd;\n\t\t\t} else {\n\t\t\t\t// We are in a --dry-run scenario .. this might have been a directory we 'faked' doing something with. \n\t\t\t\t// pathsRenamed contains all the paths that were 'renamed'\n\t\t\t\tif (pathsRenamed.canFind(ensureStartsWithDotSlash(buildNormalizedPath(pathToAdd)))) {\n\t\t\t\t\t// Path was renamed .. but faked due to --dry-run\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"DRY-RUN: Skipping creating this directory online as this was a faked local change\", [\"debug\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// Add this to the list to create online\n\t\t\t\t\tpathsToCreateOnline ~= pathToAdd;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Create new directories online\n\tvoid processNewDirectoriesToCreateOnline() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// This list of local paths that need to be created online\n\t\tstring[] uniquePathsToCreateOnline;\n\t\t\n\t\t// Are there any new local directories to create online?\n\t\tif (!pathsToCreateOnline.empty) {\n\t\t\t// There are new directories to create online\n\t\t\taddLogEntry(\"New directories to create on Microsoft OneDrive: \" ~ to!string(pathsToCreateOnline.length));\n\t\t\tif (debugLogging) {addLogEntry(\"pathsToCreateOnline = \" ~ to!string(pathsToCreateOnline), [\"debug\"]);}\n\t\t\t\n\t\t\t// Process 'pathsToCreateOnline' into each array element, then create each path based on path segments\n\t\t\tforeach (fullPath; pathsToCreateOnline) {\n\t\t\t\t// Normalise path and strip leading \"./\" if present\n\t\t\t\tstring normalised = fullPath;\n\t\t\t\tif (normalised.startsWith(\"./\"))\n\t\t\t\t\tnormalised = normalised[2 .. $];\n\t\t\t\tif (normalised.endsWith(\"/\"))\n\t\t\t\t\tnormalised = normalised[0 .. $ - 1];\n\n\t\t\t\tauto segments = normalised.split(\"/\").filter!(s => !s.empty).array;\n\t\t\t\tstring pathToCreate = \".\";\n\n\t\t\t\tforeach (i; 0 .. segments.length) {\n\t\t\t\t\tpathToCreate = buildPath(pathToCreate, segments[i]);\n\t\t\t\t\t\n\t\t\t\t\t// Only add unique paths to avoid duplication of the same path creation request\n\t\t\t\t\tif (!uniquePathsToCreateOnline.canFind(pathToCreate)) {\n\t\t\t\t\t\t// Add this unique path to the created online\n\t\t\t\t\t\tuniquePathsToCreateOnline ~= pathToCreate;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Now that all the paths have been rationalised and potential duplicate creation requests filtered out, create the paths online\n\t\tif (debugLogging) {addLogEntry(\"uniquePathsToCreateOnline = \" ~ to!string(uniquePathsToCreateOnline), [\"debug\"]);}\n\t\t\n\t\t// For each path in the array, attempt to create this online\n\t\tforeach (onlinePathToCreate; uniquePathsToCreateOnline) {\n\t\t\ttry {\n\t\t\t\t// Try and create the required path online\n\t\t\t\tcreateDirectoryOnline(onlinePathToCreate);\n\t\t\t} catch (Exception e) {\n\t\t\t\taddLogEntry(\"ERROR: Failed to create directory online: \" ~ onlinePathToCreate ~ \" => \" ~ e.msg);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Upload new data that has been identified to Microsoft OneDrive\n\tvoid processNewLocalItemsToUpload() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\n\t\t// Are there any new local items to upload?\n\t\tif (!newLocalFilesToUploadToOneDrive.empty) {\n\t\t\t// There are elements to upload\n\t\t\taddLogEntry(\"New items to upload to Microsoft OneDrive: \" ~ to!string(newLocalFilesToUploadToOneDrive.length) );\n\t\t\t\n\t\t\t// Reset totalDataToUpload\n\t\t\ttotalDataToUpload = 0;\n\t\t\t\n\t\t\t// 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\n\t\t\tforeach (uploadFilePath; newLocalFilesToUploadToOneDrive) {\n\t\t\t\t// validate that the path actually exists so that it can be counted\n\t\t\t\tif (exists(uploadFilePath)) {\n\t\t\t\t\ttotalDataToUpload = totalDataToUpload + getSize(uploadFilePath);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// How much data is there to upload\n\t\t\tif (verboseLogging) {\n\t\t\t\tif (totalDataToUpload < 1024) {\n\t\t\t\t\t// Display as Bytes to upload\n\t\t\t\t\taddLogEntry(\"Total New Data to Upload:        \" ~ to!string(totalDataToUpload) ~ \" Bytes\", [\"verbose\"]);\n\t\t\t\t} else {\n\t\t\t\t\tif ((totalDataToUpload > 1024) && (totalDataToUpload < 1048576)) {\n\t\t\t\t\t\t// Display as KB to upload\n\t\t\t\t\t\taddLogEntry(\"Total New Data to Upload:        \" ~ to!string((totalDataToUpload / 1024)) ~ \" KB\", [\"verbose\"]);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Display as MB to upload\n\t\t\t\t\t\taddLogEntry(\"Total New Data to Upload:        \" ~ to!string((totalDataToUpload / 1024 / 1024)) ~ \" MB\", [\"verbose\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// How much space is available \n\t\t\t// The file, could be uploaded to a shared folder, which, we are not tracking how much free space is available there ... \n\t\t\t// Iterate through all the drives we have cached thus far, that we know about\n\t\t\tif (debugLogging) {\n\t\t\t\tforeach (driveId, driveDetails; onlineDriveDetails) {\n\t\t\t\t\t// Log how much space is available for each driveId\n\t\t\t\t\taddLogEntry(\"Current Available Space Online (\" ~ driveId ~ \"): \" ~ to!string((driveDetails.quotaRemaining / 1024 / 1024)) ~ \" MB\", [\"debug\"]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Perform the upload\n\t\t\tuploadNewLocalFileItems();\n\t\t\t\n\t\t\t// Cleanup array memory after uploading all files\n\t\t\tnewLocalFilesToUploadToOneDrive = [];\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Scan this path for new data\n\tvoid scanPathForNewData(string path) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Skip symlinks as early as possible, including dangling symlinks\n\t\tif (isSymlink(path)) {\n\t\t\t// Should this path be skipped?\n\t\t\tif (appConfig.getValueBool(\"skip_symlinks\")) {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping item - skip symbolic links configured: \" ~ path, [\"verbose\"]);}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Add a processing '.' if path exists\n\t\tif (exists(path)) {\n\t\t\tif (isDir(path)) {\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t\t\taddProcessingDotEntry();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlong maxPathLength;\n\t\tlong pathWalkLength;\n\t\t\n\t\t// Add this logging break to assist with what was checked for each path\n\t\tif (path != \".\") {\n\t\t\tif (debugLogging) {addLogEntry(debugLogBreakType1, [\"debug\"]);}\n\t\t}\n\t\t\n\t\t// https://support.microsoft.com/en-us/help/3125202/restrictions-and-limitations-when-you-sync-files-and-folders\n\t\t// If the path is greater than allowed characters, then one drive will return a '400 - Bad Request'\n\t\t// Need to ensure that the URI is encoded before the check is made:\n\t\t// - 400 Character Limit for OneDrive Business / Office 365\n\t\t// - 430 Character Limit for OneDrive Personal\n\t\t\n\t\t// Configure maxPathLength based on account type\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Personal Account\n\t\t\tmaxPathLength = 430;\n\t\t} else {\n\t\t\t// Business Account / Office365 / SharePoint\n\t\t\tmaxPathLength = 400;\n\t\t}\n\t\t\n\t\t// 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 ...\n\t\t// 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\n\t\t// In this case, we need to skip adding this new local file - we do not upload it (we cant , and we should not)\n\t\tif (appConfig.accountType == \"business\") {\n\t\t\t// Check appConfig.configuredBusinessSharedFilesDirectoryName against 'path'\n\t\t\tif (canFind(path, baseName(appConfig.configuredBusinessSharedFilesDirectoryName))) {\n\t\t\t\t// Log why this path is being skipped\n\t\t\t\taddLogEntry(\"Skipping scanning path for new files as this is reserved for OneDrive Business Shared Files: \" ~ path, [\"info\"]);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// A short lived item that has already disappeared will cause an error - is the path still valid?\n\t\tif (!exists(path)) {\n\t\t\taddLogEntry(\"Skipping path - path has disappeared: \" ~ path);\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Calculate the path length by walking the path and catch any UTF-8 sequence errors at the same time\n\t\t// https://github.com/skilion/onedrive/issues/57\n\t\t// https://github.com/abraunegg/onedrive/issues/487\n\t\t// https://github.com/abraunegg/onedrive/issues/1192\n\t\ttry {\n\t\t\tpathWalkLength = path.byGrapheme.walkLength;\n\t\t} catch (std.utf.UTFException e) {\n\t\t\t// Path contains characters which generate a UTF exception\n\t\t\taddLogEntry(\"Skipping item - invalid UTF sequence: \" ~ path, [\"info\", \"notify\"]);\n\t\t\tif (debugLogging) {addLogEntry(\"  Error Reason:\" ~ e.msg, [\"debug\"]);}\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Is the path length is less than maxPathLength\n\t\tif (pathWalkLength < maxPathLength) {\n\t\t\t// Is this path unwanted\n\t\t\tbool unwanted = false;\n\t\t\t\n\t\t\t// First check of this item - if we are in a --dry-run scenario, we may have 'fake deleted' this path\n\t\t\t// 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\n\t\t\t// Check this 'path' for an entry in pathFakeDeletedArray - if it is there, this is unwanted\n\t\t\tif (dryRun) {\n\t\t\t\t// Is this path in the array of fake deleted items? If yes, return early, nothing else to do, save processing\n\t\t\t\tif (canFind(pathFakeDeletedArray, path)) return;\n\t\t\t}\n\t\t\t\n\t\t\t// Check if item if found in database\n\t\t\tbool itemFoundInDB = pathFoundInDatabase(path);\n\t\t\t\n\t\t\t// If the item is already found in the database, it is redundant to perform these checks\n\t\t\tif (!itemFoundInDB) {\n\t\t\t\t// This not a Client Side Filtering check, nor a Microsoft Check, but is a sanity check that the path provided is UTF encoded correctly\n\t\t\t\t// Check the std.encoding of the path against: Unicode 5.0, ASCII, ISO-8859-1, ISO-8859-2, WINDOWS-1250, WINDOWS-1251, WINDOWS-1252\n\t\t\t\tif (!unwanted) {\n\t\t\t\t\tif(!isValid(path)) {\n\t\t\t\t\t\t// Path is not valid according to https://dlang.org/phobos/std_encoding.html\n\t\t\t\t\t\taddLogEntry(\"Skipping item - invalid character encoding sequence: \" ~ path, [\"info\", \"notify\"]);\n\t\t\t\t\t\tunwanted = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Check this path against the Client Side Filtering Rules\n\t\t\t\t// - check_nosync\n\t\t\t\t// - skip_dotfiles\n\t\t\t\t// - skip_symlinks\n\t\t\t\t// - skip_file\n\t\t\t\t// - skip_dir\n\t\t\t\t// - sync_list\n\t\t\t\t// - skip_size\n\t\t\t\tif (!unwanted) {\n\t\t\t\t\t// 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'\n\t\t\t\t\tif (!cleanupDataPass) {\n\t\t\t\t\t\tunwanted = checkPathAgainstClientSideFiltering(path);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Check this path against the Microsoft Naming Conventions & Restrictions\n\t\t\t\t// - Check path against Microsoft OneDrive restriction and limitations about Windows naming for files and folders\n\t\t\t\t// - Check path for bad whitespace items\n\t\t\t\t// - Check path for HTML ASCII Codes\n\t\t\t\t// - Check path for ASCII Control Codes\n\t\t\t\tif (!unwanted) {\n\t\t\t\t\tunwanted = checkPathAgainstMicrosoftNamingRestrictions(path);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Before we traverse this 'path', we need to make a last check to see if this was just excluded\n\t\t\tbool skipFolderTraverse = skipBusinessSharedFolder(path);\n\t\t\t\n\t\t\t// Current path for error logging\n\t\t\tstring currentPath;\n\t\t\t\n\t\t\tif (!unwanted) {\n\t\t\t\t// At this point, this path, we want to scan for new data as it is not excluded\n\t\t\t\tif (isDir(path)) {\n\t\t\t\t\t// Was the path found in the database?\n\t\t\t\t\tif (!itemFoundInDB) {\n\t\t\t\t\t\t// Path not found in database when searching all drive id's\n\t\t\t\t\t\tif (!cleanupLocalFiles) {\n\t\t\t\t\t\t\t// --download-only --cleanup-local-files not used\n\t\t\t\t\t\t\t// Create this directory on OneDrive so that we can upload files to it\n\t\t\t\t\t\t\t// Add this path to an array so that the directory online can be created before we upload files\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Adding path to create online (directory inclusion): \" ~ path, [\"debug\"]);}\n\t\t\t\t\t\t\taddPathToCreateOnline(path);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// we need to clean up this directory\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Attempting removal of local directory as --download-only & --cleanup-local-files configured\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Remove any children of this path if they still exist\n\t\t\t\t\t\t\t// Resolve 'Directory not empty' error when deleting local files\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t// the cleanup code should only operate on the immediate children of the current directory\n\t\t\t\t\t\t\t\tauto directoryEntries = dirEntries(path, SpanMode.shallow);\n\n\t\t\t\t\t\t\t\tforeach (DirEntry child; directoryEntries) {\n\t\t\t\t\t\t\t\t\t// Normalise the child path once and use it consistently everywhere\n\t\t\t\t\t\t\t\t\tstring normalisedChildPath = ensureStartsWithDotSlash(buildNormalizedPath(child.name));\n\n\t\t\t\t\t\t\t\t\t// Default action is to remove unless a retention condition is met\n\t\t\t\t\t\t\t\t\tbool pathShouldBeRemoved = true;\n\n\t\t\t\t\t\t\t\t\t// 1. If this path was already retained earlier, never delete it\n\t\t\t\t\t\t\t\t\tif (canFind(pathsRetained, normalisedChildPath)) {\n\t\t\t\t\t\t\t\t\t\tpathShouldBeRemoved = false;\n\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Path already marked for retention - retaining path: \" ~ normalisedChildPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// 2. If not already retained, evaluate via sync_list\n\t\t\t\t\t\t\t\t\tif (pathShouldBeRemoved && syncListConfigured) {\n\t\t\t\t\t\t\t\t\t\t// selectiveSync.isPathExcludedViaSyncList() returns:\n\t\t\t\t\t\t\t\t\t\t//   true  = excluded by sync_list\n\t\t\t\t\t\t\t\t\t\t//   false = included / must be retained\n\t\t\t\t\t\t\t\t\t\tif (!selectiveSync.isPathExcludedViaSyncList(child.name)) {\n\t\t\t\t\t\t\t\t\t\t\tpathShouldBeRemoved = false;\n\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Path retained due to 'sync_list' inclusion: \" ~ normalisedChildPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// What action should be taken?\n\t\t\t\t\t\t\t\t\tif (pathShouldBeRemoved) {\n\t\t\t\t\t\t\t\t\t\t// Path should be removed\n\t\t\t\t\t\t\t\t\t\tif (isDir(child.name)) {\n\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Attempting removal of local directory: \" ~ normalisedChildPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Attempting removal of local file: \" ~ normalisedChildPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t// Are we in a --dry-run scenario?\n\t\t\t\t\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\t\t\t\t\t// No --dry-run ... process local delete\n\t\t\t\t\t\t\t\t\t\t\tif (exists(child.name)) {\n\t\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif (attrIsDir(child.linkAttributes)) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trmdir(child.name);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Log removal\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Removed local directory: \" ~ normalisedChildPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsafeRemove(child.name);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Log removal\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Removed local file: \" ~ normalisedChildPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, normalisedChildPath);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Path should be retained\n\t\t\t\t\t\t\t\t\t\tif (isDir(child.name)) {\n\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Local directory should be retained due to 'sync_list' inclusion: \" ~ child.name, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Local file should be retained due to 'sync_list' inclusion: \" ~ child.name, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t// Add this path to the retention list if not already present\n\t\t\t\t\t\t\t\t\t\tif (!canFind(pathsRetained, normalisedChildPath)) {\n\t\t\t\t\t\t\t\t\t\t\tpathsRetained ~= normalisedChildPath;\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t// Child retained, do not perform any further delete logic for this child\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Clear directoryEntries\n\t\t\t\t\t\t\t\tobject.destroy(directoryEntries);\n\n\t\t\t\t\t\t\t\t// Determine whether the parent path itself should be removed\n\t\t\t\t\t\t\t\tbool parentalPathShouldBeRemoved = true;\n\t\t\t\t\t\t\t\tstring normalisedParentPath = ensureStartsWithDotSlash(buildNormalizedPath(path));\n\t\t\t\t\t\t\t\tstring parentPrefix = normalisedParentPath ~ \"/\";\n\n\t\t\t\t\t\t\t\t// 1. sync_list evaluation for the parent path itself\n\t\t\t\t\t\t\t\tif (syncListConfigured) {\n\t\t\t\t\t\t\t\t\t// selectiveSync.isPathExcludedViaSyncList() returns:\n\t\t\t\t\t\t\t\t\t//   true  = excluded by sync_list\n\t\t\t\t\t\t\t\t\t//   false = included / must be retained\n\t\t\t\t\t\t\t\t\tif (!selectiveSync.isPathExcludedViaSyncList(path)) {\n\t\t\t\t\t\t\t\t\t\tparentalPathShouldBeRemoved = false;\n\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Parent path retained due to 'sync_list' inclusion: \" ~ path, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// 2. If parent path exists in the database, it must be retained\n\t\t\t\t\t\t\t\tif (parentalPathShouldBeRemoved && pathFoundInDatabase(normalisedParentPath)) {\n\t\t\t\t\t\t\t\t\tparentalPathShouldBeRemoved = false;\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Parent path found in database - retain path: \" ~ normalisedParentPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// 3. If any retained path is this parent or is beneath this parent, retain the parent\n\t\t\t\t\t\t\t\tif (parentalPathShouldBeRemoved) {\n\t\t\t\t\t\t\t\t\tforeach (retainedPath; pathsRetained) {\n\t\t\t\t\t\t\t\t\t\tif ((retainedPath == normalisedParentPath) || retainedPath.startsWith(parentPrefix)) {\n\t\t\t\t\t\t\t\t\t\t\tparentalPathShouldBeRemoved = false;\n\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Parent path retained because child path is retained: \" ~ retainedPath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// What action should be taken?\n\t\t\t\t\t\t\t\tif (parentalPathShouldBeRemoved) {\n\t\t\t\t\t\t\t\t\t// Remove the parental path now that it is empty of children\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Attempting removal of local directory: \" ~ path, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// are we in a --dry-run scenario?\n\t\t\t\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\t\t\t\t// No --dry-run ... process local delete\n\t\t\t\t\t\t\t\t\t\tif (exists(path)) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\trmdirRecurse(path);\n\t\t\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Removed local directory: \" ~ path, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t\t\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, path);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Path needs to be retained\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Local parent directory should be retained due to 'sync_list' inclusion: \" ~ path, [\"verbose\"]);}\n\n\t\t\t\t\t\t\t\t\t// Add the parent path to the retention list if not already present\n\t\t\t\t\t\t\t\t\tif (!canFind(pathsRetained, normalisedParentPath)) {\n\t\t\t\t\t\t\t\t\t\tpathsRetained ~= normalisedParentPath;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t\t\t\t// display the error message\n\t\t\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, currentPath);\n\n\t\t\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// return as there was an error\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Do we actually traverse this path?\n\t\t\t\t\tif (!skipFolderTraverse) {\n\t\t\t\t\t\t// Try and access this directory and any path below\n\t\t\t\t\t\tif (exists(path)) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tauto directoryEntries = dirEntries(path, SpanMode.shallow, false);\n\t\t\t\t\t\t\t\tforeach (DirEntry entry; directoryEntries) {\n\t\t\t\t\t\t\t\t\tcurrentPath = entry.name;\n\t\t\t\t\t\t\t\t\tscanPathForNewData(entry.name);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Clear directoryEntries\n\t\t\t\t\t\t\t\tobject.destroy(directoryEntries);\n\t\t\t\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t\t\t\t// display the error message\n\t\t\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, currentPath);\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// return as there was an error\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// https://github.com/abraunegg/onedrive/issues/984\n\t\t\t\t\t// path is not a directory, is it a valid file?\n\t\t\t\t\t// pipes - whilst technically valid files, are not valid for this client\n\t\t\t\t\t//  prw-rw-r--.  1 user user    0 Jul  7 05:55 my_pipe\n\t\t\t\t\tif (isFile(path)) {\n\t\t\t\t\t\t// Is the file a '.nosync' file?\n\t\t\t\t\t\tif (canFind(path, \".nosync\")) {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Skipping .nosync file\", [\"debug\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// return as there was an error\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t\t// Was the file found in the database?\n\t\t\t\t\t\tif (!itemFoundInDB) {\n\t\t\t\t\t\t\t// File not found in database when searching all drive id's\n\t\t\t\t\t\t\t// Do we upload the file or clean up the file?\n\t\t\t\t\t\t\tif (!cleanupLocalFiles) {\n\t\t\t\t\t\t\t\t// --download-only --cleanup-local-files not used\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Ensure this directory on OneDrive so that we can upload files to it\n\t\t\t\t\t\t\t\t// Add this path to an array so that the directory online can be created before we upload files\n\t\t\t\t\t\t\t\tstring parentPath = dirName(path);\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Adding parental path to create online (file inclusion): \" ~ parentPath, [\"debug\"]);}\n\t\t\t\t\t\t\t\taddPathToCreateOnline(parentPath);\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Add this path as a file we need to upload\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"OneDrive Client flagging to upload this file to Microsoft OneDrive: \" ~ path, [\"debug\"]);}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\t\t\t// Add to the array\n\t\t\t\t\t\t\t\t\tnewLocalFilesToUploadToOneDrive ~= path;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// In a --dry-run scenario, we may have locally fake changed a directory name, thus, this path we are checking needs to checked against 'pathsRenamed'\n\t\t\t\t\t\t\t\t\tif (pathsRenamed.canFind(ensureStartsWithDotSlash(buildNormalizedPath(parentPath)))) {\n\t\t\t\t\t\t\t\t\t\t// Parental path was renamed\n\t\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"DRY-RUN: parentPath found in 'pathsRenamed' ... skipping uploading this file\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Add to the array\n\t\t\t\t\t\t\t\t\t\tnewLocalFilesToUploadToOneDrive ~= path;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Normalise the file path once and use it consistently everywhere\n\t\t\t\t\t\t\t\tstring normalisedFilePath = ensureStartsWithDotSlash(buildNormalizedPath(path));\n\n\t\t\t\t\t\t\t\t// Default action is to remove unless a retention condition is met\n\t\t\t\t\t\t\t\tbool pathShouldBeRemoved = true;\n\n\t\t\t\t\t\t\t\t// 1. If this path was already retained earlier, never delete it\n\t\t\t\t\t\t\t\tif (canFind(pathsRetained, normalisedFilePath)) {\n\t\t\t\t\t\t\t\t\tpathShouldBeRemoved = false;\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Path already marked for retention - retaining path: \" ~ normalisedFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// 2. If not already retained, evaluate via sync_list\n\t\t\t\t\t\t\t\tif (pathShouldBeRemoved && syncListConfigured) {\n\t\t\t\t\t\t\t\t\t// selectiveSync.isPathExcludedViaSyncList() returns:\n\t\t\t\t\t\t\t\t\t//   true  = excluded by sync_list\n\t\t\t\t\t\t\t\t\t//   false = included / must be retained\n\t\t\t\t\t\t\t\t\tif (!selectiveSync.isPathExcludedViaSyncList(path)) {\n\t\t\t\t\t\t\t\t\t\tpathShouldBeRemoved = false;\n\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Path retained due to 'sync_list' inclusion: \" ~ normalisedFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// What action should be taken?\n\t\t\t\t\t\t\t\tif (pathShouldBeRemoved) {\n\t\t\t\t\t\t\t\t\t// we need to clean up this file\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Attempting removal of local file as --download-only & --cleanup-local-files configured\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t// are we in a --dry-run scenario?\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Attempting removal of local file: \" ~ normalisedFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\t\t\t\t// No --dry-run ... process local file delete\n\t\t\t\t\t\t\t\t\t\tsafeRemove(path);\n\t\t\t\t\t\t\t\t\t\t// Log removal\n\t\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Removed local file: \" ~ normalisedFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Path should be retained\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Local file should be retained due to 'sync_list' inclusion: \" ~ normalisedFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Add this path to the retention list if not already present\n\t\t\t\t\t\t\t\t\tif (!canFind(pathsRetained, normalisedFilePath)) {\n\t\t\t\t\t\t\t\t\t\tpathsRetained ~= normalisedFilePath;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// path is not a valid file\n\t\t\t\t\t\taddLogEntry(\"Skipping item - item is not a valid file: \" ~ path, [\"info\", \"notify\"]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Issue #3126 - https://github.com/abraunegg/onedrive/discussions/3126\n\t\t\t\t// 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 ...\n\t\t\t\t// 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,\n\t\t\t\t// 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, \n\t\t\t\t// but the child content has to be included\n\t\t\t\tif (isDir(path)) {\n\t\t\t\t\t// Do we actually traverse this path?\n\t\t\t\t\tif (!skipFolderTraverse) {\n\t\t\t\t\t\t// Not a Business Shared Folder that must not be traversed if 'sync_business_shared_folders' is not enabled\n\t\t\t\t\t\t// Was this path excluded by the 'sync_list' exclusion process\n\t\t\t\t\t\tif (syncListDirExcluded) {\n\t\t\t\t\t\t\t// yes .. this parent path was excluded by the 'sync_list' ... we need to scan this path for potential new data that may be included\n\t\t\t\t\t\t\tbool parentalInclusionSyncListRule = selectiveSync.isSyncListPrefixMatch(path);\n\t\t\t\t\t\t\tbool syncListAnywhereInclusionRulesExist = selectiveSync.syncListAnywhereInclusionRulesExist();\n\t\t\t\t\t\t\tbool mustTraversePath = false;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif ((parentalInclusionSyncListRule) || (syncListAnywhereInclusionRulesExist)) {\n\t\t\t\t\t\t\t\tmustTraversePath = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Log what we are testing\n\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\taddLogEntry(\"Local path was excluded by 'sync_list' but is this in anyway included in a specific 'inclusion' rule?\", [\"debug\"]);\n\t\t\t\t\t\t\t\t// Is this path in the 'sync_list' inclusion path array?\n\t\t\t\t\t\t\t\taddLogEntry(\"Testing path against the specific 'sync_list' inclusion rules: \" ~ path, [\"debug\"]);\n\t\t\t\t\t\t\t\taddLogEntry(\"Should we traverse this local path to scan for new data: \" ~ to!string(mustTraversePath), [\"debug\"]);\n\t\t\t\t\t\t\t\taddLogEntry(\" - parentalInclusionSyncListRule: \" ~ to!string(parentalInclusionSyncListRule), [\"debug\"]);\n\t\t\t\t\t\t\t\taddLogEntry(\" - syncListAnywhereInclusionRulesExist:    \" ~ to!string(syncListAnywhereInclusionRulesExist), [\"debug\"]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Was traversal of this excluded path triggered?\n\t\t\t\t\t\t\tif (mustTraversePath) {\n\t\t\t\t\t\t\t\t// We must traverse this path .. \n\t\t\t\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\t\t\t\t// Why ...\n\t\t\t\t\t\t\t\t\tif (syncListAnywhereInclusionRulesExist) {\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Bypassing 'sync_list' exclusion to scan directory for potential new data that may be included due to 'sync_list' anywhere rule existence\", [\"verbose\"]);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Bypassing 'sync_list' exclusion to scan directory for potential new data that may be included\", [\"verbose\"]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Try and go through the excluded directory path\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tauto directoryEntries = dirEntries(path, SpanMode.shallow, false);\n\t\t\t\t\t\t\t\t\tforeach (DirEntry entry; directoryEntries) {\n\t\t\t\t\t\t\t\t\t\tcurrentPath = entry.name;\n\t\t\t\t\t\t\t\t\t\tscanPathForNewData(entry.name);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Clear directoryEntries\n\t\t\t\t\t\t\t\t\tobject.destroy(directoryEntries);\n\t\t\t\t\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t\t\t\t\t// display the error message\n\t\t\t\t\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, currentPath);\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// return as there was an error\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// This path was skipped - why?\n\t\t\taddLogEntry(\"Skipping item '\" ~ path ~ \"' due to the full path exceeding \" ~ to!string(maxPathLength) ~ \" characters (Microsoft OneDrive limitation)\", [\"info\", \"notify\"]);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Do we skip this path as it might be an Online Business Shared Folder\n\tbool skipBusinessSharedFolder(string path) {\n\t\t// Is this a business account?\n\t\tif (appConfig.accountType == \"business\") {\n\t\t\t// search businessSharedFoldersOnlineToSkip for this path\n\t\t\tif (canFind(businessSharedFoldersOnlineToSkip, path)) {\n\t\t\t\t// This path was skipped - why?\n\t\t\t\taddLogEntry(\"Skipping item '\" ~ path ~ \"' due to this path matching an existing online Business Shared Folder name\", [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry(\"To sync this Business Shared Folder, consider enabling 'sync_business_shared_folders' within your application configuration.\", [\"info\"]);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\n\t\t// return value\n\t\treturn false;\n\t}\n\t\n\t// Handle a single file inotify trigger when using --monitor\n\tvoid handleLocalFileTrigger(string[] changedLocalFilesToUploadToOneDrive) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Is this path a new file or an existing one?\n\t\t// Normally we would use pathFoundInDatabase() to calculate, but we need 'databaseItem' as well if the item is in the database\n\t\tforeach (localFilePath; changedLocalFilesToUploadToOneDrive) {\n\t\t\ttry {\n\t\t\t\tItem databaseItem;\n\t\t\t\tbool fileFoundInDB = false;\n\t\t\t\t\n\t\t\t\tforeach (driveId; onlineDriveDetails.keys) {\n\t\t\t\t\tif (itemDB.selectByPath(localFilePath, driveId, databaseItem)) {\n\t\t\t\t\t\tfileFoundInDB = true;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// file found, search no more\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Was the file found in the database?\n\t\t\t\tif (!fileFoundInDB) {\n\t\t\t\t\t// This is a new file as it is not in the database\n\t\t\t\t\t// Log that the file has been added locally\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"[M] New local file added: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\tscanLocalFilesystemPathForNewDataToUpload(localFilePath);\n\t\t\t\t} else {\n\t\t\t\t\t// This is a potentially modified file, needs to be handled as such. Is the item truly modified?\n\t\t\t\t\tif (!testFileHash(localFilePath, databaseItem)) {\n\t\t\t\t\t\t// The local file failed the hash comparison test - there is a data difference\n\t\t\t\t\t\t// Log that the file has changed locally\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"[M] Local file changed: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\t\t\t// Add the modified item to the array to upload\n\t\t\t\t\t\tuploadChangedLocalFileToOneDrive([databaseItem.driveId, databaseItem.id, localFilePath]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch(Exception e) {\n\t\t\t\taddLogEntry(\"Cannot upload file changes/creation: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t}\n\t\t}\n\t\tprocessNewLocalItemsToUpload();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Query the database to determine if this path is within the existing database\n\tbool pathFoundInDatabase(string searchPath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Normalise input IF required\n\t\tif (!startsWith(searchPath, \"./\")) {\n\t\t\tif (searchPath != \".\") {\n\t\t\t\t// Log that the path needs normalising\n\t\t\t\tif (debugLogging) {addLogEntry(\"searchPath does not start with './' ... searchPath needs normalising\", [\"debug\"]);}\n\t\t\t\tsearchPath = ensureStartsWithDotSlash(buildNormalizedPath(searchPath));\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Check if this path in the database\n\t\tItem databaseItem;\n\t\tif (debugLogging) {addLogEntry(\"Search DB for this path: \" ~ searchPath, [\"debug\"]);}\n\t\t\n\t\tforeach (driveId; onlineDriveDetails.keys) {\n\t\t\tif (itemDB.selectByPath(searchPath, driveId, databaseItem)) {\n\t\t\t\tif (debugLogging) {addLogEntry(\"DB Record for search path: \" ~ to!string(databaseItem), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (debugLogging) {addLogEntry(\"Path found in database - early exit\", [\"debug\"]);}\n\t\t\t\treturn true; // Early exit on finding the path in the DB\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\tif (debugLogging) {addLogEntry(\"Path not found in database after exhausting all driveId entries: \" ~ searchPath, [\"debug\"]);}\n\t\treturn false; // Return false if path is not found in any drive\n\t}\n\t\n\t// Create a new directory online on OneDrive\n\t// - Test if we can get the parent path details from the database, otherwise we need to search online\n\t//   for the path flow and create the folder that way\n\tvoid createDirectoryOnline(string thisNewPathToCreate) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Is this a valid path to create?\n\t\t// We need to avoid this sort of error:\n\t\t//\n\t\t//\t\tOneDrive generated an error when creating this path: .\n\n\t\t//\t\tERROR: Microsoft OneDrive API returned an error with the following message:\n\t\t//\t\t  Error Message:       HTTP request returned status code 400 (Bad Request)\n\t\t//\t\t  Error Reason:        Invalid request\n\t\t//\t\t  Error Code:          invalidRequest\n\t\t//\t\t  Error Timestamp:     2025-08-01T21:08:26\n\t\t//\t\t  API Request ID:      dca77bd6-1e9a-432a-bc6c-1c6b5380745d\n\t\tif (isRootEquivalent(thisNewPathToCreate)) return;\n\t\t\n\t\t// Log what path we are attempting to create online\n\t\tif (verboseLogging) {addLogEntry(\"OneDrive Client requested to create this directory online: \" ~ thisNewPathToCreate, [\"verbose\"]);}\n\t\t\n\t\t// Function variables\n\t\tItem parentItem;\n\t\tJSONValue onlinePathData;\n\t\t\n\t\t// Special Folder Handling: Do NOT create the folder online if it is being used for OneDrive Business Shared Files\n\t\t// These are local copy files, in a self created directory structure which is not to be replicated online\n\t\t// Check appConfig.configuredBusinessSharedFilesDirectoryName against 'thisNewPathToCreate'\n\t\tif (canFind(thisNewPathToCreate, baseName(appConfig.configuredBusinessSharedFilesDirectoryName))) {\n\t\t\t// Log why this is being skipped\n\t\t\taddLogEntry(\"Skipping creating '\" ~ thisNewPathToCreate ~ \"' as this path is used for handling OneDrive Business Shared Files\", [\"info\", \"notify\"]);\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\n\t\t\t// return early as skipping\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi createDirectoryOnlineOneDriveApiInstance;\n\t\tcreateDirectoryOnlineOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tcreateDirectoryOnlineOneDriveApiInstance.initialise();\n\t\t\n\t\t// What parent path to use?\n\t\tstring parentPath = dirName(thisNewPathToCreate); // will be either . or something else\n\t\t\n\t\t// Configure the parentItem by if this is the account 'root' use the root details, or search the database for the parent details\n\t\tif (parentPath == \".\") {\n\t\t\t// Parent path is '.' which is the account root\n\t\t\t// Use client defaults\n\t\t\tparentItem.driveId = appConfig.defaultDriveId;\n\t\t\tparentItem.id = appConfig.defaultRootId;\n\t\t} else {\n\t\t\t// Query the parent path online\n\t\t\tif (debugLogging) {addLogEntry(\"Attempting to query Local Database for this parent path: \" ~ parentPath, [\"debug\"]);}\n\n\t\t\t// Attempt a 2 step process to work out where to create the directory\n\t\t\t// Step 1: Query the DB first for the parent path, to try and avoid an API call\n\t\t\t// Step 2: Query online as last resort\n\t\t\t\n\t\t\t// Step 1: Check if this parent path in the database\n\t\t\tItem databaseItem;\n\t\t\tbool parentPathFoundInDB = false;\n\t\t\t\n\t\t\tforeach (driveId; onlineDriveDetails.keys) {\n\t\t\t\t// driveId comes from the DB .. trust it is has been validated\n\t\t\t\tif (debugLogging) {addLogEntry(\"Query DB with this driveID for the Parent Path: \" ~ driveId, [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Query the database for this parent path using each driveId that we know about\n\t\t\t\tif (itemDB.selectByPath(parentPath, driveId, databaseItem)) {\n\t\t\t\t\tparentPathFoundInDB = true;\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"Parent databaseItem: \" ~ to!string(databaseItem), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"parentPathFoundInDB: \" ~ to!string(parentPathFoundInDB), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Set parentItem to the item returned from the database\n\t\t\t\t\tparentItem = databaseItem;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// After querying all DB entries for each driveID for the parent path, what are the details in parentItem?\n\t\t\tif (debugLogging) {addLogEntry(\"Parent parentItem after DB Query exhausted: \" ~ to!string(parentItem), [\"debug\"]);}\n\n\t\t\t// Step 2: Query for the path online if NOT found in the local database\n\t\t\tif (!parentPathFoundInDB) {\n\t\t\t\t// parent path not found in database\n\t\t\t\ttry {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Attempting to query OneDrive Online for this parent path as path not found in local database: \" ~ parentPath, [\"debug\"]);}\n\t\t\t\t\tonlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetails(parentPath);\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Online Parent Path Query Response: \" ~ to!string(onlinePathData), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Make the parentItem from the online data\n\t\t\t\t\tparentItem = makeItem(onlinePathData);\n\t\t\t\t\t\n\t\t\t\t\t// Before we 'save' this item to the database, is the parent of this parent in the database?\n\t\t\t\t\t// We need to go and check the grandparent item for this parent item\n\t\t\t\t\tItem grandparentDatabaseItem;\n\t\t\t\t\tbool grandparentInDatabase = itemDB.selectById(onlinePathData[\"parentReference\"][\"driveId\"].str, onlinePathData[\"parentReference\"][\"id\"].str, grandparentDatabaseItem);\n\t\t\t\t\t\n\t\t\t\t\t// Is the 'grandparent' in the database?\n\t\t\t\t\tif (!grandparentInDatabase) {\n\t\t\t\t\t\t// No ..\n\t\t\t\t\t\tstring grandParentPath = dirName(parentPath);\n\t\t\t\t\t\t// create/add grandparent path online, add to database\n\t\t\t\t\t\tcreateDirectoryOnline(grandParentPath);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Save parent item to the database\n\t\t\t\t\tsaveItem(onlinePathData);\n\t\t\t\t\t\n\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t\t\t// Parent does not exist ... need to create parent\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Parent path does not exist online: \" ~ parentPath, [\"debug\"]);}\n\t\t\t\t\t\tcreateDirectoryOnline(parentPath);\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\tonlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetails(parentPath);\n\t\t\t\t\t\tparentItem = makeItem(onlinePathData);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Make sure the full path does not exist online, this should generate a 404 response, to which then the folder will be created online\n\t\ttry {\n\t\t\t// Try and query the OneDrive API for the path we need to create\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"Attempting to query OneDrive API for this path: \" ~ thisNewPathToCreate, [\"debug\"]);\n\t\t\t\taddLogEntry(\"parentItem details: \" ~ to!string(parentItem), [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Depending on the data within parentItem, will depend on what method we are using to search\n\t\t\t// A Shared Folder will be 'remote' so we need to check the remote parent id, rather than parentItem details\n\t\t\tItem queryItem;\n\t\t\t\n\t\t\t// If we are doing a normal sync, 'parentItem.type == ItemType.remote' comparison works\n\t\t\t// If we are doing a --local-first 'parentItem.type == ItemType.remote' fails as the returned object is not a remote item, but is remote based on the 'driveId'\n\t\t\tif (parentItem.type == ItemType.remote) {\n\t\t\t\t// This folder is a potential shared object\n\t\t\t\tif (debugLogging) {addLogEntry(\"ParentItem is a remote item object\", [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Is this a Personal Account Type or has 'sync_business_shared_items' been enabled?\n\t\t\t\tif ((appConfig.accountType == \"personal\") || (appConfig.getValueBool(\"sync_business_shared_items\"))) {\n\t\t\t\t\t// Update the queryItem values\n\t\t\t\t\tqueryItem.driveId = parentItem.remoteDriveId;\n\t\t\t\t\tqueryItem.id = parentItem.remoteId;\n\t\t\t\t} else {\n\t\t\t\t\t// This is a shared folder location, but we are not a 'personal' account, and 'sync_business_shared_items' has not been enabled\n\t\t\t\t\taddLogEntry(\"ERROR: Unable to create directory online as 'sync_business_shared_items' is not enabled\");\n\t\t\t\t\t\n\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// return as we cannot continue here\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Use parent item for the query item\n\t\t\t\tif (debugLogging) {addLogEntry(\"Standard Query, use parentItem\", [\"debug\"]);}\n\t\t\t\tqueryItem = parentItem;\n\t\t\t}\n\t\t\t\n\t\t\t// Issue #3115 - Validate driveId length\n\t\t\t// What account type is this?\n\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\tqueryItem.driveId = transformToLowerCase(queryItem.driveId);\n\t\t\t\t\n\t\t\t\t// Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\t\tif (queryItem.driveId != appConfig.defaultDriveId) {\n\t\t\t\t\tqueryItem.driveId = testProvidedDriveIdForLengthIssue(queryItem.driveId);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tif (queryItem.driveId == appConfig.defaultDriveId) {\n\t\t\t\t// Use getPathDetailsByDriveId\n\t\t\t\tif (debugLogging) {addLogEntry(\"Selecting getPathDetailsByDriveId to query OneDrive API for path data\", [\"debug\"]);}\n\t\t\t\tonlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetailsByDriveId(queryItem.driveId, thisNewPathToCreate);\n\t\t\t} else {\n\t\t\t\t// Use searchDriveForPath to query OneDrive\n\t\t\t\tif (debugLogging) {addLogEntry(\"Selecting searchDriveForPath to query OneDrive API for path data\", [\"debug\"]);}\n\t\t\t\t// If the queryItem.driveId is not our driveId - the path we are looking for will not be at the logical location that getPathDetailsByDriveId \n\t\t\t\t// can use - as it will always return a 404 .. even if the path actually exists (which is the whole point of this test)\n\t\t\t\t// Search the queryItem.driveId for any folder name match that we are going to create, then compare response JSON items with queryItem.id\n\t\t\t\t// 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\n\t\t\t\tonlinePathData = createDirectoryOnlineOneDriveApiInstance.searchDriveForPath(queryItem.driveId, baseName(thisNewPathToCreate));\n\t\t\t\tif (debugLogging) {addLogEntry(\"onlinePathData: \" ~to!string(onlinePathData), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Process the response from searching the drive\n\t\t\t\tlong responseCount = count(onlinePathData[\"value\"].array);\n\t\t\t\tif (responseCount > 0) {\n\t\t\t\t\t// Search 'name' matches were found .. need to match these against queryItem.id\n\t\t\t\t\tbool foundDirectoryOnline = false;\n\t\t\t\t\tJSONValue foundDirectoryJSONItem;\n\t\t\t\t\t// Items were returned .. but is one of these what we are looking for?\n\t\t\t\t\tforeach (childJSON; onlinePathData[\"value\"].array) {\n\t\t\t\t\t\t// Is this item not a file?\n\t\t\t\t\t\tif (!isFileItem(childJSON)) {\n\t\t\t\t\t\t\tItem thisChildItem = makeItem(childJSON);\n\t\t\t\t\t\t\t// Direct Match Check\n\t\t\t\t\t\t\tif ((queryItem.id == thisChildItem.parentId) && (baseName(thisNewPathToCreate) == thisChildItem.name)) {\n\t\t\t\t\t\t\t\t// High confidence that this child folder is a direct match we are trying to create and it already exists online\n\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Path we are searching for exists online (Direct Match): \" ~ baseName(thisNewPathToCreate), [\"debug\"]);\n\t\t\t\t\t\t\t\t\taddLogEntry(\"childJSON: \" ~ sanitiseJSONItem(childJSON), [\"debug\"]);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfoundDirectoryOnline = true;\n\t\t\t\t\t\t\t\tfoundDirectoryJSONItem = childJSON;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Full Lower Case POSIX Match Check\n\t\t\t\t\t\t\tstring childAsLower = toLower(childJSON[\"name\"].str);\n\t\t\t\t\t\t\tstring thisFolderNameAsLower = toLower(baseName(thisNewPathToCreate));\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Child name check\n\t\t\t\t\t\t\tif (childAsLower == thisFolderNameAsLower) {\t\n\t\t\t\t\t\t\t\t// This is a POSIX 'case in-sensitive match' ..... in folder name only\n\t\t\t\t\t\t\t\t// - Local item name has a 'case-insensitive match' to an existing item on OneDrive\n\t\t\t\t\t\t\t\t// The 'parentId' of this JSON object must match the parentId of where the folder was created\n\t\t\t\t\t\t\t\t// - why .. we might have the same folder name, but somewhere totally different\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tif (queryItem.id == thisChildItem.parentId) {\n\t\t\t\t\t\t\t\t\t// Found the directory in the location, using case in-sensitive matching\n\t\t\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Path we are searching for exists online (POSIX 'case in-sensitive match'): \" ~ baseName(thisNewPathToCreate), [\"debug\"]);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"childJSON: \" ~ sanitiseJSONItem(childJSON), [\"debug\"]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tfoundDirectoryOnline = true;\n\t\t\t\t\t\t\t\t\tfoundDirectoryJSONItem = childJSON;\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (foundDirectoryOnline) {\n\t\t\t\t\t\t// Directory we are seeking was found online ...\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"The directory we are seeking was found online by using searchDriveForPath ...\", [\"debug\"]);}\n\t\t\t\t\t\tonlinePathData = foundDirectoryJSONItem;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No 'search item matches found' - raise a 404 so that the exception handling will take over to create the folder\n\t\t\t\t\t\tthrow new OneDriveException(404, \"Name not found via search\");\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// No 'search item matches found' - raise a 404 so that the exception handling will take over to create the folder\n\t\t\t\t\tthrow new OneDriveException(404, \"Name not found via search\");\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (OneDriveException exception) {\n\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t// This is a good error - it means that the directory to create 100% does not exist online\n\t\t\t\t// The directory was not found on the drive id we queried\n\t\t\t\tif (verboseLogging) {addLogEntry(\"The requested directory to create was not found on OneDrive - creating remote directory: \" ~ thisNewPathToCreate, [\"verbose\"]);}\n\t\t\t\t\n\t\t\t\t// Build up the online create directory request\n\t\t\t\tstring requiredDriveId;\n\t\t\t\tstring requiredParentItemId;\n\t\t\t\tJSONValue createDirectoryOnlineAPIResponse;\n\t\t\t\tJSONValue newDriveItem = [\n\t\t\t\t\t\t\"name\": JSONValue(baseName(thisNewPathToCreate)),\n\t\t\t\t\t\t\"folder\": parseJSON(\"{}\")\n\t\t\t\t];\n\t\t\t\t\n\t\t\t\t// Submit the creation request\n\t\t\t\t// Fix for https://github.com/skilion/onedrive/issues/356\n\t\t\t\tif (!dryRun) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Attempt to create a new folder on the required driveId and parent item id\n\t\t\t\t\t\t// Is the item a Remote Object (Shared Folder) ?\n\t\t\t\t\t\tif (parentItem.type == ItemType.remote) {\n\t\t\t\t\t\t\t// Yes .. Shared Folder\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"parentItem data: \" ~ to!string(parentItem), [\"debug\"]);}\n\t\t\t\t\t\t\trequiredDriveId = parentItem.remoteDriveId;\n\t\t\t\t\t\t\trequiredParentItemId = parentItem.remoteId;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Not a Shared Folder\n\t\t\t\t\t\t\trequiredDriveId = parentItem.driveId;\n\t\t\t\t\t\t\trequiredParentItemId = parentItem.id;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Where are we creating this new folder?\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"requiredDriveId:      \" ~ requiredDriveId, [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"requiredParentItemId: \" ~ requiredParentItemId, [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"newDriveItem JSON:    \" ~ sanitiseJSONItem(newDriveItem), [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t\t// Create the new folder\n\t\t\t\t\t\tcreateDirectoryOnlineAPIResponse = createDirectoryOnlineOneDriveApiInstance.createById(requiredDriveId, requiredParentItemId, newDriveItem);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Log that the directory was created\n\t\t\t\t\t\taddLogEntry(\"Successfully created the remote directory \" ~ thisNewPathToCreate ~ \" on Microsoft OneDrive\");\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is the response a valid JSON object - validation checking done in saveItem, printing of the JSON object is done in saveItem()\n\t\t\t\t\t\tsaveItem(createDirectoryOnlineAPIResponse);\n\t\t\t\t\t\t\n\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\tif (exception.httpStatusCode == 409) {\n\t\t\t\t\t\t\t// OneDrive API returned a 404 (far above) to say the directory did not exist\n\t\t\t\t\t\t\t// but when we attempted to create it, OneDrive responded that it now already exists with a 409\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"OneDrive reported that \" ~ thisNewPathToCreate ~ \" already exists .. OneDrive API race condition\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Try to recover race condition by querying the parent's children for the folder we are trying to create\n\t\t\t\t\t\t\tcreateDirectoryOnlineAPIResponse = resolveOnlineCreationRaceCondition(requiredDriveId, requiredParentItemId, thisNewPathToCreate);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Log that the directory details were obtained\n\t\t\t\t\t\t\taddLogEntry(\"Successfully obtained the remote directory details \" ~ thisNewPathToCreate ~ \" from Microsoft OneDrive\");\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Is the response a valid JSON object - validation checking done in saveItem, printing of the JSON object is done in saveItem()\n\t\t\t\t\t\t\tsaveItem(createDirectoryOnlineAPIResponse);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Shutdown this API instance, as we will create API instances as required, when required\n\t\t\t\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\t\t// Free object and memory\n\t\t\t\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance = null;\n\t\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// some other error from OneDrive was returned - display what it is\n\t\t\t\t\t\t\taddLogEntry(\"OneDrive generated an error when creating this path: \" ~ thisNewPathToCreate);\n\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t\t// Shutdown this API instance, as we will create API instances as required, when required\n\t\t\t\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\t\t// Free object and memory\n\t\t\t\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance = null;\n\t\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// return due to OneDriveException\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Simulate a successful 'directory create' & save it to the dryRun database copy\n\t\t\t\t\taddLogEntry(\"Successfully created the remote directory \" ~ thisNewPathToCreate ~ \" on Microsoft OneDrive\");\n\t\t\t\t\t// The simulated response has to pass 'makeItem' as part of saveItem\n\t\t\t\t\tauto fakeResponse = createFakeResponse(thisNewPathToCreate);\n\t\t\t\t\t// Save item to the database\n\t\t\t\t\tsaveItem(fakeResponse);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Shutdown this API instance, as we will create API instances as required, when required\n\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t// Free object and memory\n\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// shutdown & return\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within createDirectoryOnlineOneDriveApiInstance\n\t\t\t\t\n\t\t\t\t// If we get a 400 error, there is an issue creating this folder on Microsoft OneDrive for some reason\n\t\t\t\t// If the error is not 400, re-try, else fail\n\t\t\t\tif (exception.httpStatusCode != 400) {\n\t\t\t\t\t// Attempt a re-try\n\t\t\t\t\tcreateDirectoryOnline(thisNewPathToCreate);\n\t\t\t\t} else {\n\t\t\t\t\t// We cant create this directory online\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"This folder cannot be created online: \" ~ buildNormalizedPath(absolutePath(thisNewPathToCreate)), [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// If we get to this point - onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, thisNewPathToCreate) generated a 'valid' response ....\n\t\t// This means that the folder potentially exists online .. which is odd .. as it should not have existed\n\t\tif (onlinePathData.type() == JSONType.object) {\n\t\t\t// A valid object was responded with\n\t\t\tif (onlinePathData[\"name\"].str == baseName(thisNewPathToCreate)) {\n\t\t\t\t// OneDrive 'name' matches local path name\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"The path to query/search for online was found online\", [\"debug\"]);\n\t\t\t\t\taddLogEntry(\" onlinePathData via query/search: \" ~ to!string(onlinePathData), [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Now we know the location of this folder via query/search - go get the actual path details using the 'onlinePathData'\n\t\t\t\tItem onlineItem = makeItem(onlinePathData);\n\t\t\t\t\n\t\t\t\t// Fetch the real data in a consistent manner to ensure the JSON response contains the elements we are expecting\n\t\t\t\tJSONValue realOnlinePathData;\n\t\t\t\t\n\t\t\t\t// Get drive details for the provided driveId\n\t\t\t\ttry {\n\t\t\t\t\trealOnlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetailsById(onlineItem.driveId, onlineItem.id);\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\" realOnlinePathData via getPathDetailsById call: \" ~ to!string(realOnlinePathData), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t// An error was generated\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"realOnlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetailsById(onlineItem.driveId, onlineItem.id) generated a OneDriveException\", [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t// Display what the error is\n\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\n\t\t\t\t\t// abort ..\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// OneDrive Personal Shared Folder Check - Use the REAL online data here\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t// 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\n\t\t\t\t\t// Is this a remote folder\n\t\t\t\t\tif (isItemRemote(realOnlinePathData)) {\n\t\t\t\t\t\t// The folder is a remote item ...\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"The existing Online Folder and 'realOnlinePathData' indicate this is most likely a OneDrive Personal Shared Folder Link added by 'Add shortcut to My files'\", [\"debug\"]);}\n\t\t\t\t\t\t// It is a 'remote' JSON item denoting a potential shared folder\n\t\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(realOnlinePathData);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// OneDrive Business Shared Folder Check\n\t\t\t\tif (appConfig.accountType == \"business\") {\n\t\t\t\t\t// 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\n\t\t\t\t\t// Is this a remote folder\n\t\t\t\t\tif (isItemRemote(realOnlinePathData)) {\n\t\t\t\t\t\t// The folder is a remote item ... \n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"The existing Online Folder and 'realOnlinePathData' indicate this is most likely a OneDrive Shared Business Folder Link added by 'Add shortcut to My files'\", [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is Shared Business Folder Syncing actually enabled?\n\t\t\t\t\t\tif (!appConfig.getValueBool(\"sync_business_shared_items\")) {\n\t\t\t\t\t\t\t// Shared Business Folder Syncing is NOT enabled\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"We need to skip this path: \" ~ thisNewPathToCreate, [\"debug\"]);}\n\t\t\t\t\t\t\t// Add this path to businessSharedFoldersOnlineToSkip\n\t\t\t\t\t\t\tbusinessSharedFoldersOnlineToSkip ~= [thisNewPathToCreate];\n\t\t\t\t\t\t\t// no save to database, no online create\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance = null;\n\t\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// return due to skipped path\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Shared Business Folder Syncing IS enabled\n\t\t\t\t\t\t\t// It is a 'remote' JSON item denoting a potential shared folder\n\t\t\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(realOnlinePathData);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Path found online\n\t\t\t\tif (verboseLogging) {addLogEntry(\"The requested directory to create was found on OneDrive - skipping creating the directory online: \" ~ thisNewPathToCreate, [\"verbose\"]);}\n\t\t\t\t// Is the response a valid JSON object - validation checking done in saveItem\n\t\t\t\tsaveItem(realOnlinePathData);\n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// return due to path found online\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\t// Normally this would throw an error, however we cant use throw new PosixException()\n\t\t\t\tstring 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);\n\t\t\t\tdisplayPosixErrorMessage(msg);\n\t\t\t\taddLogEntry(\"ERROR: Requested directory to create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online.\");\n\t\t\t\taddLogEntry(\"ERROR: To resolve, rename this local directory: \" ~ buildNormalizedPath(absolutePath(thisNewPathToCreate)));\n\t\t\t\taddLogEntry(\"Skipping creating this directory online due to 'case-insensitive match': \" ~ thisNewPathToCreate);\n\t\t\t\t// Add this path to posixViolationPaths\n\t\t\t\tposixViolationPaths ~= [thisNewPathToCreate];\n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\tcreateDirectoryOnlineOneDriveApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// manual POSIX exception\n\t\t\t\treturn;\n\t\t\t}\n\t\t} else {\n\t\t\t// response is not valid JSON, an error was returned from OneDrive\n\t\t\taddLogEntry(\"ERROR: There was an error performing this operation on Microsoft OneDrive\");\n\t\t\taddLogEntry(\"ERROR: Increase logging verbosity to assist determining why.\");\n\t\t\taddLogEntry(\"Skipping: \" ~ buildNormalizedPath(absolutePath(thisNewPathToCreate)));\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tcreateDirectoryOnlineOneDriveApiInstance.releaseCurlEngine();\n\t\t\tcreateDirectoryOnlineOneDriveApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\t// generic error\n\t\t\treturn;\n\t\t}\n\t}\n\t\n\t// In the event that the online creation triggered a 404 then a 409 on creation attempt, this function explicitly is used to query that parent for the child being sought\n\t// This should return a usable JSON response of the folder being sought\n\tJSONValue resolveOnlineCreationRaceCondition(string requiredDriveId, string requiredParentItemId, string thisNewPathToCreate) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi raceConditionResolutionOneDriveApiInstance;\n\t\traceConditionResolutionOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\traceConditionResolutionOneDriveApiInstance.initialise();\n\t\t\n\t\t// What is the folder we are seeking\n\t\tstring searchFolder = baseName(thisNewPathToCreate);\n\t\t\n\t\t// Where should we store the details of the online folder we are seeking?\n\t\tJSONValue targetOnlineFolderDetails;\n\t\t\n\t\t// Required variables for listChildren to operate\n\t\tJSONValue topLevelChildren;\n\t\tstring nextLink;\n\t\tbool directoryFoundOnline = false;\n\t\t\n\t\t// To handle ^c events, we need this Code\n\t\twhile (true) {\n\t\t\t// Check if exitHandlerTriggered is true\n\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t\n\t\t\t// Query this remote object for its children\n\t\t\ttopLevelChildren = raceConditionResolutionOneDriveApiInstance.listChildren(requiredDriveId, requiredParentItemId, nextLink);\n\t\t\t\n\t\t\t// Process each child that has been returned\n\t\t\tforeach (child; topLevelChildren[\"value\"].array) {\n\t\t\t\t// We are specifically seeking a 'folder' object\n\t\t\t\tif (isItemFolder(child)) {\n\t\t\t\t\t// Is this the child folder we are looking for, and is a POSIX match?\n\t\t\t\t\t// We know that Microsoft OneDrive is not POSIX aware, thus there cannot be 2 folders of the same name with different case sensitivity\n\t\t\t\t\tif (child[\"name\"].str == searchFolder) {\n\t\t\t\t\t\t// EXACT MATCH including case sensitivity: Flag that we found the folder online\n\t\t\t\t\t\tdirectoryFoundOnline = true;\n\t\t\t\t\t\t// Use these details for raceCondition response\n\t\t\t\t\t\ttargetOnlineFolderDetails = child;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstring childAsLower = toLower(child[\"name\"].str);\n\t\t\t\t\t\tstring thisFolderNameAsLower = toLower(searchFolder);\n\t\t\t\t\t\t\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (childAsLower == thisFolderNameAsLower) {\t\n\t\t\t\t\t\t\t\t// This is a POSIX 'case in-sensitive match' ..... \n\t\t\t\t\t\t\t\t// Local item name has a 'case-insensitive match' to an existing item on OneDrive\n\t\t\t\t\t\t\t\tthrow new PosixException(searchFolder, child[\"name\"].str);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (PosixException e) {\n\t\t\t\t\t\t\t// Display POSIX error message\n\t\t\t\t\t\t\tdisplayPosixErrorMessage(e.msg);\n\t\t\t\t\t\t\taddLogEntry(\"ERROR: Requested directory to search for and potentially create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online.\");\n\t\t\t\t\t\t\taddLogEntry(\"ERROR: To resolve, rename this local directory: \" ~ thisNewPathToCreate);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// That set of returned objects - did we find the folder?\n\t\t\tif (directoryFoundOnline) {\n\t\t\t\t// We found the folder, no need to continue searching nextLink data\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t\n\t\t\t// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response \n\t\t\t// to indicate more items are available and provide the request URL for the next page of items.\n\t\t\tif (\"@odata.nextLink\" in topLevelChildren) {\n\t\t\t\t// Update nextLink to next changeSet bundle\n\t\t\t\tif (debugLogging) {addLogEntry(\"Setting nextLink to (@odata.nextLink): \" ~ nextLink, [\"debug\"]);}\n\t\t\t\tnextLink = topLevelChildren[\"@odata.nextLink\"].str;\n\t\t\t} else break;\n\t\t\t\n\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t}\n\t\t\n\t\t// Shutdown this API instance, as we will create API instances as required, when required\n\t\traceConditionResolutionOneDriveApiInstance.releaseCurlEngine();\n\t\t// Free object and memory\n\t\traceConditionResolutionOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return the JSON with the folder details\n\t\treturn targetOnlineFolderDetails;\n\t}\n\t\n\t// Test that the online name actually matches the requested local name\n\tbool performPosixTest(string localNameToCheck, string onlineName) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file\n\t\t// Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same, \n\t\t// even though some file systems (such as a POSIX-compliant file system) may consider them as different. \n\t\t// Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior.\n\t\tbool posixIssue = false;\n\t\t\n\t\t// Check for a POSIX casing mismatch\n\t\tif (localNameToCheck != onlineName) {\n\t\t\t// The input items are different .. how are they different?\n\t\t\tif (toLower(localNameToCheck) == toLower(onlineName)) {\n\t\t\t\t// Names differ only by case -> POSIX issue\n\t\t\t\tif (debugLogging) {addLogEntry(\"performPosixTest: Names differ only by case -> POSIX issue\", [\"debug\"]);}\n\t\t\t\t// Local item name has a 'case-insensitive match' to an existing item on OneDrive\n\t\t\t\tposixIssue = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return the posix evaluation\n\t\treturn posixIssue;\n\t}\n\t\n\t// Upload new file items as identified\n\tvoid uploadNewLocalFileItems() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\n\t\t// Lets deal with the new local items in a batch process\n\t\tsize_t batchSize = to!int(appConfig.getValueLong(\"threads\"));\n\t\tlong batchCount = (newLocalFilesToUploadToOneDrive.length + batchSize - 1) / batchSize;\n\t\tlong batchesProcessed = 0;\n\t\t\n\t\t// Transfer order\n\t\tstring transferOrder = appConfig.getValueString(\"transfer_order\");\n\t\t\n\t\t// Has the user configured to specify the transfer order of files?\n\t\tif (transferOrder != \"default\") {\n\t\t\t// If we have more than 1 item to upload, sort the items\n\t\t\tif (count(newLocalFilesToUploadToOneDrive) > 1) {\n\t\t\t\t// Create an array of tuples (file path, file size)\n\t\t\t\tauto fileInfo = newLocalFilesToUploadToOneDrive\n\t\t\t\t\t.map!(file => tuple(file, getSize(file))) // Get file size for each file that needs to be uploaded\n\t\t\t\t\t.array;\n\n\t\t\t\t// Perform sorting based on transferOrder\n\t\t\t\tif (transferOrder == \"size_asc\") {\n\t\t\t\t\tfileInfo.sort!((a, b) => a[1] < b[1]); // sort the array by ascending size\n\t\t\t\t} else if (transferOrder == \"size_dsc\") {\n\t\t\t\t\tfileInfo.sort!((a, b) => a[1] > b[1]); // sort the array by descending size\n\t\t\t\t} else if (transferOrder == \"name_asc\") {\n\t\t\t\t\tfileInfo.sort!((a, b) => a[0] < b[0]); // sort the array by ascending name\n\t\t\t\t} else if (transferOrder == \"name_dsc\") {\n\t\t\t\t\tfileInfo.sort!((a, b) => a[0] > b[0]); // sort the array by descending name\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Extract sorted file paths\n\t\t\t\tnewLocalFilesToUploadToOneDrive = fileInfo.map!(t => t[0]).array;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Process newLocalFilesToUploadToOneDrive\n\t\tforeach (chunk; newLocalFilesToUploadToOneDrive.chunks(batchSize)) {\n\t\t\t// send an array containing 'appConfig.getValueLong(\"threads\")' local files to upload\n\t\t\tuploadNewLocalFileItemsInParallel(chunk);\n\t\t}\n\t\t\n\t\t// For this set of items, perform a DB PASSIVE checkpoint\n\t\titemDB.performCheckpoint(\"PASSIVE\");\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Upload the file batches in parallel\n\tvoid uploadNewLocalFileItemsInParallel(string[] array) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// This function received an array of string items to upload, the number of elements based on appConfig.getValueLong(\"threads\")\n\t\tforeach (i, fileToUpload; processPool.parallel(array)) {\n\t\t\tif (debugLogging) {addLogEntry(\"Upload Thread \" ~ to!string(i) ~ \" Starting: \" ~ to!string(Clock.currTime()), [\"debug\"]);}\n\t\t\tuploadNewFile(fileToUpload);\n\t\t\tif (debugLogging) {addLogEntry(\"Upload Thread \" ~ to!string(i) ~ \" Finished: \" ~ to!string(Clock.currTime()), [\"debug\"]);}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Upload a new file to OneDrive\n\tvoid uploadNewFile(string fileToUpload) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Debug for the moment\n\t\tif (debugLogging) {addLogEntry(\"fileToUpload: \" ~ fileToUpload, [\"debug\"]);}\n\t\t\n\t\t// These are the details of the item we need to upload\n\t\t// How much space is remaining on OneDrive\n\t\tlong remainingFreeSpaceOnline;\n\t\t// Did the upload fail?\n\t\tbool uploadFailed = false;\n\t\t// Did we skip due to exceeding maximum allowed size?\n\t\tbool skippedMaxSize = false;\n\t\t// Did we skip to an exception error?\n\t\tbool skippedExceptionError = false;\n\t\t// Is the parent path in the item database?\n\t\tbool parentPathFoundInDB = false;\n\t\t// Get this file size\n\t\tlong thisFileSize;\n\t\t// Is there space available online\n\t\tbool spaceAvailableOnline = false;\n\t\t// Flag to track if there is zero data traversal\n\t\tbool zeroDataTraversal = false;\n\t\t\n\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\tlong calculatedSpaceOnlinePostUpload;\n\t\t\n\t\tOneDriveApi checkFileOneDriveApiInstance;\n\t\t\n\t\t// Check the database for the parent path of fileToUpload\n\t\tItem parentItem;\n\t\t// What parent path to use?\n\t\tstring parentPath = dirName(fileToUpload); // will be either . or something else\n\t\tif (parentPath == \".\"){\n\t\t\t// Assume this is a new file in the users configured sync_dir root\n\t\t\t// Use client defaults\n\t\t\tparentItem.id = appConfig.defaultRootId;  \t\t// Should give something like 12345ABCDE1234A1!101\n\t\t\tparentItem.driveId = appConfig.defaultDriveId; \t// Should give something like 12345abcde1234a1\n\t\t\tparentPathFoundInDB = true;\n\t\t} else {\n\t\t\t// Query the database using each of the driveId's we are using\n\t\t\tforeach (driveId; onlineDriveDetails.keys) {\n\t\t\t\t// Query the database for this parent path using each driveId\n\t\t\t\tItem dbResponse;\n\t\t\t\tif(itemDB.selectByPath(parentPath, driveId, dbResponse)){\n\t\t\t\t\t// parent path was found in the database\n\t\t\t\t\tparentItem = dbResponse;\n\t\t\t\t\tparentPathFoundInDB = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// If the parent path was found in the DB, to ensure we are uploading the right location 'parentItem.driveId' must not be empty\n\t\tif ((parentPathFoundInDB) && (parentItem.driveId.empty)) {\n\t\t\t// switch to using defaultDriveId\n\t\t\tif (debugLogging) {addLogEntry(\"parentItem.driveId is empty - using defaultDriveId for upload API calls\", [\"debug\"]);}\n\t\t\tparentItem.driveId = appConfig.defaultDriveId;\n\t\t}\n\t\t\n\t\t// Check if the path still exists locally before we try to upload\n\t\tif (exists(fileToUpload)) {\n\t\t\t// Can we read the file - as a permissions issue or actual file corruption will cause a failure\n\t\t\t// Resolves: https://github.com/abraunegg/onedrive/issues/113\n\t\t\tif (readLocalFile(fileToUpload)) {\n\t\t\t\t// The local file can be read - so we can read it to attempt to upload it in this thread\n\t\t\t\t// Is the path parent in the DB?\n\t\t\t\tif (parentPathFoundInDB) {\n\t\t\t\t\t// Parent path is in the database\n\t\t\t\t\t// Get the new file size\n\t\t\t\t\t// Even if the permissions on the file are: -rw-------.  1 root root    8 Jan 11 09:42\n\t\t\t\t\t// we can still obtain the file size, however readLocalFile() also tests if the file can be read (permission check)\n\t\t\t\t\tthisFileSize = getSize(fileToUpload);\n\t\t\t\t\t\n\t\t\t\t\t// Does this file exceed the maximum filesize for OneDrive\n\t\t\t\t\t// Resolves: https://github.com/skilion/onedrive/issues/121 , https://github.com/skilion/onedrive/issues/294 , https://github.com/skilion/onedrive/issues/329\n\t\t\t\t\tif (thisFileSize <= maxUploadFileSize) {\n\t\t\t\t\t\t// Is there enough free space on OneDrive as compared to when we started this thread, to safely upload the file to OneDrive?\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Make sure that parentItem.driveId is in our driveIDs array to use when checking if item is in database\n\t\t\t\t\t\t// Keep the DriveDetailsCache array with unique entries only\n\t\t\t\t\t\tif (!canFindDriveId(parentItem.driveId, cachedOnlineDriveData)) {\n\t\t\t\t\t\t\t// Add this driveId to the drive cache, which then also sets for the defaultDriveId:\n\t\t\t\t\t\t\t// - quotaRestricted;\n\t\t\t\t\t\t\t// - quotaAvailable;\n\t\t\t\t\t\t\t// - quotaRemaining;\n\t\t\t\t\t\t\taddOrUpdateOneDriveOnlineDetails(parentItem.driveId);\n\t\t\t\t\t\t\t// Fetch the details from cachedOnlineDriveData\n\t\t\t\t\t\t\tcachedOnlineDriveData = getDriveDetails(parentItem.driveId);\n\t\t\t\t\t\t} \n\t\t\t\t\t\t\n\t\t\t\t\t\t// Fetch the details from cachedOnlineDriveData\n\t\t\t\t\t\t// - cachedOnlineDriveData.quotaRestricted;\n\t\t\t\t\t\t// - cachedOnlineDriveData.quotaAvailable;\n\t\t\t\t\t\t// - cachedOnlineDriveData.quotaRemaining;\n\t\t\t\t\t\tremainingFreeSpaceOnline = cachedOnlineDriveData.quotaRemaining;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// When we compare the space online to the total we are trying to upload - is there space online?\n\t\t\t\t\t\tcalculatedSpaceOnlinePostUpload = remainingFreeSpaceOnline - thisFileSize;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Based on what we know, for this thread - can we safely upload this modified local file?\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\tstring estimatedMessage = format(\"This Thread (Upload New File) Estimated Free Space Online (%s): \", parentItem.driveId);\n\t\t\t\t\t\t\taddLogEntry(estimatedMessage ~ to!string(remainingFreeSpaceOnline), [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"This Thread (Upload New File) Calculated Free Space Online Post Upload: \" ~ to!string(calculatedSpaceOnlinePostUpload), [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\n\t\t\t\t\t\t// If 'personal' accounts, if driveId == defaultDriveId, then we will have data - appConfig.quotaAvailable will be updated\n\t\t\t\t\t\t// If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data - appConfig.quotaRestricted will be set as true\n\t\t\t\t\t\t// If 'business' accounts, if driveId == defaultDriveId, then we will have data\n\t\t\t\t\t\t// 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\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (remainingFreeSpaceOnline > totalDataToUpload) {\n\t\t\t\t\t\t\t// Space available\n\t\t\t\t\t\t\tspaceAvailableOnline = true;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// we need to look more granular\n\t\t\t\t\t\t\t// What was the latest getRemainingFreeSpace() value?\n\t\t\t\t\t\t\tif (cachedOnlineDriveData.quotaAvailable) {\n\t\t\t\t\t\t\t\t// 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?\n\t\t\t\t\t\t\t\tif (calculatedSpaceOnlinePostUpload > 0) {\n\t\t\t\t\t\t\t\t\t// Based on this thread action, we believe that there is space available online to upload - proceed\n\t\t\t\t\t\t\t\t\tspaceAvailableOnline = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is quota being restricted?\n\t\t\t\t\t\tif (cachedOnlineDriveData.quotaRestricted) {\n\t\t\t\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t\tparentItem.driveId = transformToLowerCase(parentItem.driveId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// If the upload target drive is not our drive id, then it is a shared folder .. we need to print a space warning message\n\t\t\t\t\t\t\tif (parentItem.driveId != appConfig.defaultDriveId) {\n\t\t\t\t\t\t\t\t// Different message depending on account type\n\t\t\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"WARNING: Shared Folder OneDrive quota information is being restricted or providing a zero value. Space available online cannot be guaranteed.\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"WARNING: OneDrive quota information is being restricted or providing a zero value. Space available online cannot be guaranteed.\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Space available online is being restricted - so we have no way to really know if there is space available online\n\t\t\t\t\t\t\tspaceAvailableOnline = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Do we have space available or is space available being restricted (so we make the blind assumption that there is space available)\n\t\t\t\t\t\tif (spaceAvailableOnline) {\n\t\t\t\t\t\t\t// We need to check that this new local file does not exist on OneDrive\n\t\t\t\t\t\t\tJSONValue fileDetailsFromOneDrive;\n\n\t\t\t\t\t\t\t// https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file\n\t\t\t\t\t\t\t// Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same, \n\t\t\t\t\t\t\t// even though some file systems (such as a POSIX-compliant file systems that Linux use) may consider them as different.\n\t\t\t\t\t\t\t// Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior, OneDrive does not use this.\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// In order to upload this file - this query HAS to respond with a '404 - Not Found' so that the upload is triggered\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Does this 'file' already exist on OneDrive?\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t// Create a new API Instance for this thread and initialise it\n\t\t\t\t\t\t\t\tcheckFileOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t\t\t\t\t\t\tcheckFileOneDriveApiInstance.initialise();\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t\t\tparentItem.driveId = transformToLowerCase(parentItem.driveId);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (parentItem.driveId == appConfig.defaultDriveId) {\n\t\t\t\t\t\t\t\t\t// getPathDetailsByDriveId is only reliable when the driveId is our driveId\n\t\t\t\t\t\t\t\t\tfileDetailsFromOneDrive = checkFileOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, fileToUpload);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// We need to curate a response by listing the children of this parentItem.driveId and parentItem.id , without traversing directories\n\t\t\t\t\t\t\t\t\t// So that IF the file is on a Shared Folder, it can be found, and, if it exists, checked correctly\n\t\t\t\t\t\t\t\t\tfileDetailsFromOneDrive = searchDriveItemForFile(parentItem.driveId, parentItem.id, fileToUpload);\n\t\t\t\t\t\t\t\t\t// Was the file found?\n\t\t\t\t\t\t\t\t\tif (fileDetailsFromOneDrive.type() != JSONType.object) {\n\t\t\t\t\t\t\t\t\t\t// No ....\n\t\t\t\t\t\t\t\t\t\tthrow new OneDriveException(404, \"Name not found via searchDriveItemForFile\");\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\t\t\tcheckFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\t\t\tcheckFileOneDriveApiInstance = null;\n\t\t\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// No 404 which means a file was found with the path we are trying to upload to\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"fileDetailsFromOneDrive JSON data after exist online check: \" ~ to!string(fileDetailsFromOneDrive), [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Portable Operating System Interface (POSIX) testing of JSON response from OneDrive API\n\t\t\t\t\t\t\t\tif (hasName(fileDetailsFromOneDrive)) {\n\t\t\t\t\t\t\t\t\t// Perform the POSIX evaluation test against the names\n\t\t\t\t\t\t\t\t\tif (performPosixTest(baseName(fileToUpload), fileDetailsFromOneDrive[\"name\"].str)) {\n\t\t\t\t\t\t\t\t\t\tthrow new PosixException(baseName(fileToUpload), fileDetailsFromOneDrive[\"name\"].str);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tthrow new JsonResponseException(\"Unable to perform POSIX test as the OneDrive API request generated an invalid JSON response\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// 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\n\t\t\t\t\t\t\t\t// and that it matches the POSIX filename of the local item we are trying to upload as a new file\n\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The file we are attempting to upload as a new file already exists on Microsoft OneDrive: \" ~ fileToUpload, [\"verbose\"]);}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// Does the data from online match our local file that we are attempting to upload as a new file?\n\t\t\t\t\t\t\t\tif (!disableUploadValidation && performUploadIntegrityValidationChecks(fileDetailsFromOneDrive, fileToUpload, thisFileSize)) {\n\t\t\t\t\t\t\t\t\t// Need a check here around the 'upload_only' and 'remove_source_files'\n\t\t\t\t\t\t\t\t\t// Are we in an --upload-only & --remove-source-files scenario?\n\t\t\t\t\t\t\t\t\tif ((uploadOnly) && (localDeleteAfterUpload)) {\n\t\t\t\t\t\t\t\t\t\t// Perform the local file deletion as the file exists online, hash matches, no upload\n\t\t\t\t\t\t\t\t\t\tremoveLocalFilePostUpload(fileToUpload);\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t// As file is now removed, we have nothing to add to the local database\n\t\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Skipping adding to database as --upload-only & --remove-source-files configured\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// No data movement, file exists online, local file matches what is online\n\t\t\t\t\t\t\t\t\t\tzeroDataTraversal = true;\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t// Save online item details to the database\n\t\t\t\t\t\t\t\t\t\tsaveItem(fileDetailsFromOneDrive);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// The local file we are attempting to upload as a new file is different to the existing file online\n\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Triggering newfile upload target already exists edge case, where the online item does not match what we are trying to upload\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Issue #2626 | Case 2-2 (resync)\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// If the 'online' file is newer, this will be overwritten with the file from the local filesystem - potentially constituting online data loss\n\t\t\t\t\t\t\t\t\t// The file 'version history' online will have to be used to 'recover' the prior online file\n\t\t\t\t\t\t\t\t\tstring changedItemParentDriveId = fileDetailsFromOneDrive[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\t\t\t\t\tstring changedItemId = fileDetailsFromOneDrive[\"id\"].str;\n\t\t\t\t\t\t\t\t\taddLogEntry(\"Skipping uploading this item as a new file, will upload as a modified file (online file already exists): \" ~ fileToUpload);\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// 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\n\t\t\t\t\t\t\t\t\tsaveItem(fileDetailsFromOneDrive);\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Which file is technically newer? The local file or the remote file?\n\t\t\t\t\t\t\t\t\tItem onlineFile = makeItem(fileDetailsFromOneDrive);\n\t\t\t\t\t\t\t\t\tSysTime localModifiedTime = timeLastModified(fileToUpload).toUTC();\n\t\t\t\t\t\t\t\t\tSysTime onlineModifiedTime = onlineFile.mtime;\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Reduce time resolution to seconds before comparing\n\t\t\t\t\t\t\t\t\tlocalModifiedTime.fracSecs = Duration.zero;\n\t\t\t\t\t\t\t\t\tonlineModifiedTime.fracSecs = Duration.zero;\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Which file is newer?\n\t\t\t\t\t\t\t\t\tif (localModifiedTime >= onlineModifiedTime) {\n\t\t\t\t\t\t\t\t\t\t// Upload the locally modified file as-is, as it is newer\n\t\t\t\t\t\t\t\t\t\tuploadChangedLocalFileToOneDrive([changedItemParentDriveId, changedItemId, fileToUpload]);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Online is newer, rename local, then upload the renamed file\n\t\t\t\t\t\t\t\t\t\t// We need to know the renamed path so we can upload it\n\t\t\t\t\t\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t\t\t\t\t\t// Rename the local path - we WANT this to occur regardless of bypassDataPreservation setting\n\t\t\t\t\t\t\t\t\t\tsafeBackup(fileToUpload, dryRun, false, renamedPath);\n\t\t\t\t\t\t\t\t\t\t// Upload renamed local file as a new file\n\t\t\t\t\t\t\t\t\t\tuploadNewFile(renamedPath);\n\t\t\t\t\t\t\t\t\t\t// Process the database entry removal for the original file. In a --dry-run scenario, this is being done against a DB copy.\n\t\t\t\t\t\t\t\t\t\t// This is done so we can download the newer online file\n\t\t\t\t\t\t\t\t\t\titemDB.deleteById(changedItemParentDriveId, changedItemId);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\t\t\tcheckFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\t\t\tcheckFileOneDriveApiInstance = null;\n\t\t\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// If we get a 404 .. the file is not online .. this is what we want .. file does not exist online\n\t\t\t\t\t\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t\t\t\t\t\t// The file has been checked, client side filtering checked, does not exist online - we need to upload it\n\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"fileDetailsFromOneDrive = checkFileOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, fileToUpload); generated a 404 - file does not exist online - must upload it\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\tuploadFailed = performNewFileUpload(parentItem, fileToUpload, thisFileSize);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// some other error\n\t\t\t\t\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} catch (PosixException e) {\n\t\t\t\t\t\t\t\t// Display POSIX error message\n\t\t\t\t\t\t\t\tdisplayPosixErrorMessage(e.msg);\n\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Requested file to upload has a 'case-insensitive match' to an existing item on Microsoft OneDrive online.\");\n\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: To resolve, rename this local file: \" ~ fileToUpload);\n\t\t\t\t\t\t\t\taddLogEntry(\"Skipping uploading this new file due to 'case-insensitive match': \" ~ fileToUpload);\n\t\t\t\t\t\t\t\tuploadFailed = true;\n\t\t\t\t\t\t\t} catch (JsonResponseException e) {\n\t\t\t\t\t\t\t\t// Display JSON error message\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(e.msg, [\"debug\"]);}\n\t\t\t\t\t\t\t\tuploadFailed = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// skip file upload - insufficient space to upload\n\t\t\t\t\t\t\taddLogEntry(\"Skipping uploading this new file as it exceeds the available free space on Microsoft OneDrive: \" ~ fileToUpload);\n\t\t\t\t\t\t\tuploadFailed = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Skip file upload - too large\n\t\t\t\t\t\taddLogEntry(\"Skipping uploading this new file as it exceeds the maximum size allowed by Microsoft OneDrive: \" ~ fileToUpload);\n\t\t\t\t\t\tuploadFailed = true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// why was the parent path not in the database?\n\t\t\t\t\tif (canFind(posixViolationPaths, parentPath)) {\n\t\t\t\t\t\taddLogEntry(\"ERROR: POSIX 'case-insensitive match' for the parent path which violates the Microsoft OneDrive API namespace convention.\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"ERROR: Parent path is not in the database or online: \" ~ parentPath);\n\t\t\t\t\t}\n\t\t\t\t\taddLogEntry(\"ERROR: Unable to upload this file: \" ~ fileToUpload);\n\t\t\t\t\tuploadFailed = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Unable to read local file\n\t\t\t\taddLogEntry(\"Skipping uploading this file as it cannot be read (file permissions or file corruption): \" ~ fileToUpload);\n\t\t\t\tuploadFailed = true;\n\t\t\t}\n\t\t} else {\n\t\t\t// File disappeared before upload\n\t\t\taddLogEntry(\"File disappeared locally before upload: \" ~ fileToUpload);\n\t\t\t// dont set uploadFailed = true; as the file disappeared before upload, thus nothing here failed\n\t\t}\n\n\t\t// Upload success or failure?\n\t\tif (!uploadFailed) {\n\t\t\t// Did we actually upload a file - that is, potentially change the online quota available state?\n\t\t\tif (!zeroDataTraversal) {\n\t\t\t\t// Update the 'cachedOnlineDriveData' record for this 'dbItem.driveId' so that this is tracked as accurately as possible for other threads\n\t\t\t\tupdateDriveDetailsCache(parentItem.driveId, cachedOnlineDriveData.quotaRestricted, cachedOnlineDriveData.quotaAvailable, thisFileSize);\n\t\t\t} else {\n\t\t\t\t// There was zero data traversal\n\t\t\t\tif (debugLogging) {addLogEntry(\"No file upload, no data movement - cachedOnlineDriveData.quotaRemaining = \" ~ to!string(cachedOnlineDriveData.quotaRemaining), [\"debug\"]);}\n\t\t\t}\n\t\t} else {\n\t\t\t// Need to add this to fileUploadFailures to capture at the end\n\t\t\tfileUploadFailures ~= fileToUpload;\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\t\n\t// Perform the actual upload to OneDrive\n\tbool performNewFileUpload(Item parentItem, string fileToUpload, long thisFileSize) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\n\t\t// Assume that by default the upload fails\n\t\tbool uploadFailed = true;\n\t\t\n\t\t// OneDrive API Upload Response\n\t\tJSONValue uploadResponse;\n\t\t\n\t\t// Create the OneDriveAPI Upload Instance\n\t\tOneDriveApi uploadFileOneDriveApiInstance;\n\t\t\n\t\t// Capture what time this upload started\n\t\tSysTime uploadStartTime = Clock.currTime();\n\t\t\n\t\t// Is this a dry-run scenario?\n\t\tif (!dryRun) {\n\t\t\t// Not a dry-run situation\n\t\t\t// Do we use simpleUpload or create an upload session?\n\t\t\tbool useSimpleUpload = false;\n\t\t\t\n\t\t\t// What upload method should be used?\n\t\t\tif (thisFileSize <= sessionThresholdFileSize) {\n\t\t\t\tuseSimpleUpload = true;\n\t\t\t}\n\t\t\t\n\t\t\t// Use Session Upload regardless\n\t\t\tif (appConfig.getValueBool(\"force_session_upload\")) {\n\t\t\t\t// Forcing session upload\n\t\t\t\tif (debugLogging) {addLogEntry(\"Forcing to perform upload using a session (newfile)\", [\"debug\"]);}\n\t\t\t\tuseSimpleUpload = false;\n\t\t\t}\n\t\t\t\n\t\t\t// We can only upload zero size files via simpleFileUpload regardless of account type\n\t\t\t// Reference: https://github.com/OneDrive/onedrive-api-docs/issues/53\n\t\t\t// Additionally, only where file size is < 4MB should be uploaded by simpleUpload - everything else should use a session to upload\n\t\t\t\n\t\t\tif ((thisFileSize == 0) || (useSimpleUpload)) { \n\t\t\t\ttry {\n\t\t\t\t\t// Initialise API for simple upload\n\t\t\t\t\tuploadFileOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t\t\t\tuploadFileOneDriveApiInstance.initialise();\n\t\t\t\t\n\t\t\t\t\t// Attempt to upload the zero byte file using simpleUpload for all account types\n\t\t\t\t\tuploadResponse = uploadFileOneDriveApiInstance.simpleUpload(fileToUpload, parentItem.driveId, parentItem.id, baseName(fileToUpload));\n\t\t\t\t\tuploadFailed = false;\n\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... done\", fileTransferNotifications());\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\tuploadFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\tuploadFileOneDriveApiInstance = null;\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\t\t\n\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t// An error was responded with - what was it\n\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t// Display what the error is\n\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\tuploadFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\tuploadFileOneDriveApiInstance = null;\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\t\t\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// display the error message\n\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, fileToUpload);\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\tuploadFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\tuploadFileOneDriveApiInstance = null;\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Initialise API for session upload\n\t\t\t\tuploadFileOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t\t\tuploadFileOneDriveApiInstance.initialise();\n\t\t\t\t\n\t\t\t\t// Session Upload for this criteria:\n\t\t\t\t// - Personal Account and file size > 4MB\n\t\t\t\t// - All Business | Office365 | SharePoint files > 0 bytes\n\t\t\t\tJSONValue uploadSessionData;\n\t\t\t\t// As this is a unique thread, the sessionFilePath for where we save the data needs to be unique\n\t\t\t\t// The best way to do this is generate a 10 digit alphanumeric string, and use this as the file extension\n\t\t\t\tstring threadUploadSessionFilePath = appConfig.uploadSessionFilePath ~ \".\" ~ generateAlphanumericString();\n\t\t\t\t\n\t\t\t\t// Attempt to upload the > 4MB file using an upload session for all account types\n\t\t\t\ttry {\n\t\t\t\t\t// Create the Upload Session\n\t\t\t\t\tuploadSessionData = createSessionForFileUpload(uploadFileOneDriveApiInstance, fileToUpload, parentItem.driveId, parentItem.id, baseName(fileToUpload), null, threadUploadSessionFilePath);\n\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t// An error was responded with - what was it\n\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t// Display what the error is\n\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t} catch (FileException e) {\n\t\t\t\t\t// display the error message\n\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, fileToUpload);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Do we have a valid session URL that we can use ?\n\t\t\t\tif (uploadSessionData.type() == JSONType.object) {\n\t\t\t\t\t// This is a valid JSON object\n\t\t\t\t\tbool sessionDataValid = true;\n\t\t\t\t\t\n\t\t\t\t\t// Validate that we have the following items which we need\n\t\t\t\t\tif (!hasUploadURL(uploadSessionData)) {\n\t\t\t\t\t\tsessionDataValid = false;\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Session data missing 'uploadUrl'\", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (!hasNextExpectedRanges(uploadSessionData)) {\n\t\t\t\t\t\tsessionDataValid = false;\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Session data missing 'nextExpectedRanges'\", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tif (!hasLocalPath(uploadSessionData)) {\n\t\t\t\t\t\tsessionDataValid = false;\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Session data missing 'localPath'\", [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\tif (sessionDataValid) {\n\t\t\t\t\t\t// We have a valid Upload Session Data we can use\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Try and perform the upload session\n\t\t\t\t\t\t\tuploadResponse = performSessionFileUpload(uploadFileOneDriveApiInstance, thisFileSize, uploadSessionData, threadUploadSessionFilePath);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (uploadResponse.type() == JSONType.object) {\n\t\t\t\t\t\t\t\tuploadFailed = false;\n\t\t\t\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... done\", fileTransferNotifications());\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t\t\t\t\tuploadFailed = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No Upload URL or nextExpectedRanges or localPath .. not a valid JSON we can use\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Session data is missing required elements to perform a session upload.\", [\"verbose\"]);}\n\t\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Create session Upload URL failed\n\t\t\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... failed!\", [\"info\", \"notify\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tuploadFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\tuploadFileOneDriveApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t}\n\t\t} else {\n\t\t\t// We are in a --dry-run scenario\n\t\t\tuploadResponse = createFakeResponse(fileToUpload);\n\t\t\tuploadFailed = false;\n\t\t\taddLogEntry(\"Uploading new file: \" ~ fileToUpload ~ \" ... done\", fileTransferNotifications());\n\t\t}\n\t\t\n\t\t// If no upload failure, calculate transfer metrics, perform integrity validation\n\t\tif (!uploadFailed) {\n\t\t\t// Upload did not fail ...\n\t\t\t// As no upload failure, calculate transfer metrics in a consistent manner\n\t\t\tdisplayTransferMetrics(fileToUpload, thisFileSize, uploadStartTime, Clock.currTime());\n\t\t\t\n\t\t\t// OK as the upload did not fail, we need to save the response from OneDrive, but it has to be a valid JSON response\n\t\t\tif (uploadResponse.type() == JSONType.object) {\n\t\t\t\t// 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\n\t\t\t\tif (exists(fileToUpload)) {\n\t\t\t\t\t// Are we in a --dry-run scenario\n\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\tbool uploadIntegrityPassed;\n\t\t\t\t\t\t// Check the integrity of the uploaded file, if the local file still exists\n\t\t\t\t\t\tuploadIntegrityPassed = performUploadIntegrityValidationChecks(uploadResponse, fileToUpload, thisFileSize);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Update the file modified time on OneDrive and save item details to database\n\t\t\t\t\t\t// Update the item's metadata on OneDrive\n\t\t\t\t\t\tSysTime mtime = timeLastModified(fileToUpload).toUTC();\n\t\t\t\t\t\tmtime.fracSecs = Duration.zero;\n\t\t\t\t\t\tstring newFileId = uploadResponse[\"id\"].str;\n\t\t\t\t\t\tstring newFileETag = uploadResponse[\"eTag\"].str;\n\t\t\t\t\t\t// Attempt to update the online date time stamp based on our local data\n\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t// Business | SharePoint we used a session to upload the data, thus, local timestamps are given when the session is created\n\t\t\t\t\t\t\tuploadLastModifiedTime(parentItem, parentItem.driveId, newFileId, mtime, newFileETag);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// 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.\n\t\t\t\t\t\t\t// This means that the file which was uploaded, is potentially no longer the file we have locally\n\t\t\t\t\t\t\t// There are 2 ways to solve this:\n\t\t\t\t\t\t\t//   1. Download the modified file immediately after upload as per v2.4.x (default)\n\t\t\t\t\t\t\t//   2. Create a new online version of the file, which then contributes to the users 'quota'\n\t\t\t\t\t\t\tif (!uploadIntegrityPassed) {\n\t\t\t\t\t\t\t\t// upload integrity check failed\n\t\t\t\t\t\t\t\t// We do not want to create a new online file version .. unless configured to do so\n\t\t\t\t\t\t\t\tif (!appConfig.getValueBool(\"create_new_file_version\")) {\n\t\t\t\t\t\t\t\t\t// are we in an --upload-only scenario\n\t\t\t\t\t\t\t\t\tif(!uploadOnly){\n\t\t\t\t\t\t\t\t\t\t// Download the now online modified file\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"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.\");\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details.\");\n\t\t\t\t\t\t\t\t\t\t// Download the file directly using the prior upload JSON response\n\t\t\t\t\t\t\t\t\t\tdownloadFileItem(uploadResponse, true);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// --upload-only being used\n\t\t\t\t\t\t\t\t\t\t// we are not downloading a file, warn that file differences will exist\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: The file uploaded to Microsoft OneDrive has been modified through its SharePoint 'enrichment' process and no longer matches your local version.\");\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: The online metadata will now be modified to match your local file which will create a new file version.\");\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"WARNING: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details.\");\n\t\t\t\t\t\t\t\t\t\t// Create a new online version of the file by updating the metadata - this ensures that the file we uploaded is the file online\n\t\t\t\t\t\t\t\t\t\tuploadLastModifiedTime(parentItem, parentItem.driveId, newFileId, mtime, newFileETag);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Create a new online version of the file by updating the metadata, which negates the need to download the file\n\t\t\t\t\t\t\t\t\tuploadLastModifiedTime(parentItem, parentItem.driveId, newFileId, mtime, newFileETag);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// integrity checks passed\n\t\t\t\t\t\t\t\t// save the uploadResponse to the database\n\t\t\t\t\t\t\t\tsaveItem(uploadResponse);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Are we in an --upload-only & --remove-source-files scenario?\n\t\t\t\t\t// Use actual config values as we are doing an upload session recovery\n\t\t\t\t\tif ((uploadOnly) && (localDeleteAfterUpload)) {\n\t\t\t\t\t\t// Perform the local file deletion\n\t\t\t\t\t\tremoveLocalFilePostUpload(fileToUpload);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// will be removed in different event!\n\t\t\t\t\taddLogEntry(\"File disappeared locally after upload: \" ~ fileToUpload);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Log that an invalid JSON object was returned\n\t\t\t\tif (debugLogging) {addLogEntry(\"uploadFileOneDriveApiInstance.simpleUpload or session.upload call returned an invalid JSON Object from the OneDrive API\", [\"debug\"]);}\n\t\t\t}\n\t\t}\n\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return upload status\n\t\treturn uploadFailed;\n\t}\n\t\n\t// Create the OneDrive Upload Session\n\tJSONValue createSessionForFileUpload(OneDriveApi activeOneDriveApiInstance, string fileToUpload, string parentDriveId, string parentId, string filename, string eTag, string threadUploadSessionFilePath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Upload file via a OneDrive API session\n\t\tJSONValue uploadSession;\n\t\t\n\t\t// Calculate modification time\n\t\tSysTime localFileLastModifiedTime = timeLastModified(fileToUpload).toUTC();\n\t\tlocalFileLastModifiedTime.fracSecs = Duration.zero;\n\t\t\n\t\t// Construct the fileSystemInfo JSON component needed to create the Upload Session\n\t\tJSONValue fileSystemInfo = [\n\t\t\t\t\"item\": JSONValue([\n\t\t\t\t\t\"@microsoft.graph.conflictBehavior\": JSONValue(\"replace\"),\n\t\t\t\t\t\"fileSystemInfo\": JSONValue([\n\t\t\t\t\t\t\"lastModifiedDateTime\": localFileLastModifiedTime.toISOExtString()\n\t\t\t\t\t])\n\t\t\t\t])\n\t\t\t];\n\t\t\n\t\t// Try to create the upload session for this file\n\t\tuploadSession = activeOneDriveApiInstance.createUploadSession(parentDriveId, parentId, filename, eTag, fileSystemInfo);\n\t\t\n\t\tif (uploadSession.type() == JSONType.object) {\n\t\t\t// a valid session object was created\n\t\t\tif (\"uploadUrl\" in uploadSession) {\n\t\t\t\t// Add the file path we are uploading to this JSON Session Data\n\t\t\t\tuploadSession[\"localPath\"] = fileToUpload;\n\t\t\t\t// Save this session\n\t\t\t\tsaveSessionFile(threadUploadSessionFilePath, uploadSession);\n\t\t\t}\n\t\t\t\n\t\t\t// When does this upload URL expire?\n\t\t\tdisplayUploadSessionExpiry(uploadSession);\n\t\t} else {\n\t\t\t// no valid session was created\n\t\t\tif (verboseLogging) {addLogEntry(\"Creation of OneDrive API Upload Session failed.\", [\"verbose\"]);}\n\t\t\t// return upload() will return a JSONValue response, create an empty JSONValue response to return\n\t\t\tuploadSession = null;\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return the JSON\n\t\treturn uploadSession;\n\t}\n\t\n\t// Display upload session expiry time\n\tvoid displayUploadSessionExpiry(JSONValue uploadSessionData) {\n\t\ttry {\n\t\t\t// Step 1: Extract the ISO 8601 UTC string from the JSON\n\t\t\tstring utcExpiry = uploadSessionData[\"expirationDateTime\"].str;\n\n\t\t\t// Step 2: Convert ISO 8601 string to SysTime (assumes Zulu / UTC timezone)\n\t\t\tSysTime expiryUTC = SysTime.fromISOExtString(utcExpiry);\n\n\t\t\t// Step 3: Convert to local time\n\t\t\tauto expiryLocal = expiryUTC.toLocalTime();\n\n\t\t\t// Step 4: Print both UTC and Local times\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"Upload session URL expires at (UTC):   \" ~ to!string(expiryUTC), [\"debug\"]);\n\t\t\t\taddLogEntry(\"Upload session URL expires at (Local): \" ~ to!string(expiryLocal), [\"debug\"]);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\t// nothing\n\t\t}\n\t}\n\t\n\t// Save the session upload data\n\tvoid saveSessionFile(string threadUploadSessionFilePath, JSONValue uploadSessionData) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tstd.file.write(threadUploadSessionFilePath, uploadSessionData.toString());\n\t\t} catch (FileException e) {\n\t\t\t// display the error message\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, threadUploadSessionFilePath);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Perform the upload of file via the Upload Session that was created\n\tJSONValue performSessionFileUpload(OneDriveApi activeOneDriveApiInstance, long thisFileSize, JSONValue uploadSessionData, string threadUploadSessionFilePath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\n\t\t// Response for upload\n\t\tJSONValue uploadResponse;\n\n\t\t// https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#upload-bytes-to-the-upload-session\n\t\t// You can upload the entire file, or split the file into multiple byte ranges, as long as the maximum bytes in any given request is less than 60 MiB.\n\t\t// Calculate File Fragment Size (must be valid multiple of 320 KiB)\n\t\tlong baseSize;\n\t\tlong fragmentSize;\n\t\tenum CHUNK_SIZE = 327_680L; // 320 KiB\n\t\tenum MAX_FRAGMENT_BYTES = 60L * 1_048_576L; // 60 MiB = 62,914,560 bytes\n\t\t\n\t\t// Time sensitive and ETA string items\n\t\tSysTime currentTime = Clock.currTime();\n\t\tlong start_unix_time = currentTime.toUnixTime();\n\t\tint h, m, s;\n\t\tstring etaString;\n\t\t\n\t\t// Upload string template\n\t\tstring uploadLogEntry = \"Uploading: \" ~ uploadSessionData[\"localPath\"].str ~ \" ... \";\n\t\t\n\t\t// Calculate base size using configured fragment size\n\t\tbaseSize = appConfig.getValueLong(\"file_fragment_size\") * 2^^20;\n\t\t\t\t\n\t\t// Ensure 'fragmentSize' is a multiple of 327680 bytes and < 60 MiB\n\t\tif (baseSize >= MAX_FRAGMENT_BYTES) {\n\t\t\t// Use the maximum valid size below 60 MiB, rounded down to nearest 320 KiB multiple\n\t\t\tfragmentSize = ((MAX_FRAGMENT_BYTES - 1) / CHUNK_SIZE) * CHUNK_SIZE;\n\t\t} else {\n\t\t\tfragmentSize = (baseSize / CHUNK_SIZE) * CHUNK_SIZE;\n\t\t}\n\t\t\n\t\t// Set the fragment count and fragSize\n\t\tsize_t fragmentCount = 0;\n\t\tlong fragSize = 0;\n\t\t\n\t\t// Extract current upload offset from session data\n\t\tlong offset = uploadSessionData[\"nextExpectedRanges\"][0].str.splitter('-').front.to!long;\n\t\t\n\t\t// Estimate total number of expected fragments\n\t\tsize_t expected_total_fragments = cast(size_t) ceil(double(thisFileSize) / double(fragmentSize));\n\t\t\n\t\t// If we get a 404, create a new upload session and store it here\n\t\tJSONValue newUploadSession;\n\t\t\n\t\t// Start the session upload using the active API instance for this thread\n\t\twhile (true) {\n\t\t\t// fragment upload\n\t\t\tfragmentCount++;\n\t\t\tif (debugLogging) {addLogEntry(\"Fragment: \" ~ to!string(fragmentCount) ~ \" of \" ~ to!string(expected_total_fragments), [\"debug\"]);}\n\n\t\t\t// Generate ETA time output\n\t\t\tetaString = formatETA(calc_eta((fragmentCount -1), expected_total_fragments, start_unix_time));\n\t\t\t\n\t\t\t// Calculate this progress output\n\t\t\tauto ratio = cast(double)(fragmentCount - 1) / expected_total_fragments;\n\t\t\t// Convert the ratio to a percentage and format it to two decimal places\n\t\t\tstring percentage = leftJustify(format(\"%d%%\", cast(int)(ratio * 100)), 5, ' ');\n\t\t\taddLogEntry(uploadLogEntry ~ percentage ~ etaString, [\"consoleOnly\"]);\n\n\t\t\t// What fragment size will be used?\n\t\t\tif (debugLogging) {addLogEntry(\"fragmentSize: \" ~ to!string(fragmentSize) ~ \" offset: \" ~ to!string(offset) ~ \" thisFileSize: \" ~ to!string(thisFileSize), [\"debug\"]);}\n\n\t\t\tfragSize = fragmentSize < thisFileSize - offset ? fragmentSize : thisFileSize - offset;\n\t\t\tif (debugLogging) {addLogEntry(\"Using fragSize: \" ~ to!string(fragSize), [\"debug\"]);}\n\n\t\t\t// fragSize must not be a negative value\n\t\t\tif (fragSize < 0) {\n\t\t\t\t// Session upload will fail\n\t\t\t\t// not a JSON object - fragment upload failed\n\t\t\t\tif (verboseLogging) {addLogEntry(\"File upload session failed - invalid calculation of fragment size\", [\"verbose\"]);}\n\n\t\t\t\tif (exists(threadUploadSessionFilePath)) {\n\t\t\t\t\tsafeRemove(threadUploadSessionFilePath);\n\t\t\t\t}\n\t\t\t\t// set uploadResponse to null as error\n\t\t\t\tuploadResponse = null;\n\t\t\t\treturn uploadResponse;\n\t\t\t}\n\n\t\t\t// If the resume upload fails, we need to check for a return code here\n\t\t\ttry {\n\t\t\t\tuploadResponse = activeOneDriveApiInstance.uploadFragment(\n\t\t\t\t\tuploadSessionData[\"uploadUrl\"].str,\n\t\t\t\t\tuploadSessionData[\"localPath\"].str,\n\t\t\t\t\toffset,\n\t\t\t\t\tfragSize,\n\t\t\t\t\tthisFileSize\n\t\t\t\t);\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// if a 100 uploadResponse is generated, continue\n\t\t\t\tif (exception.httpStatusCode == 100) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Issue #3355: https://github.com/abraunegg/onedrive/issues/3355\n\t\t\t\tif (exception.httpStatusCode == 403 && (exception.msg.canFind(\"accessDenied\") || exception.msg.canFind(\"You do not have authorization to access the file\"))) {\n\t\t\t\t\taddLogEntry(\"ERROR: Upload session has expired (403 - Access Denied)\");\n\t\t\t\t\taddLogEntry(\"Probable Cause: The 'tempauth' token embedded in the upload URL has most likely expired.\");\n\t\t\t\t\taddLogEntry(\"                Microsoft issues this token when the upload session is first created. It cannot be refreshed, extended, or queried for its expiry time.\");\n\t\t\t\t\taddLogEntry(\"                The only way to infer its validity is by measuring the time from session creation to this 403 failure.\");\n\t\t\t\t\taddLogEntry(\"                The upload session URL itself may still appear active (based on expirationDateTime), but the upload URL is no longer usable once this 'tempauth' token expires.\");\n\t\t\t\t\taddLogEntry(\"                A new upload session will now be created. Upload will restart from the beginning using the new session URL and new 'tempauth' token.\");\n\t\t\t\t\t\n\t\t\t\t\t// Attempt creation of new upload session\n\t\t\t\t\tnewUploadSession = createSessionForFileUpload(\n\t\t\t\t\t\tactiveOneDriveApiInstance,\n\t\t\t\t\t\tuploadSessionData[\"localPath\"].str,\n\t\t\t\t\t\tuploadSessionData[\"targetDriveId\"].str,\n\t\t\t\t\t\tuploadSessionData[\"targetParentId\"].str,\n\t\t\t\t\t\tbaseName(uploadSessionData[\"localPath\"].str),\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\tthreadUploadSessionFilePath\n\t\t\t\t\t);\n\t\t\t\t\t\n\t\t\t\t\t// Attempt retry (which will start upload again from scratch) with new session upload URL\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// There was an error uploadResponse from OneDrive when uploading the file fragment\n\t\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t\t// The upload session was not found .. ?? we just created it .. maybe the backend is still creating it or failed to create it\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"The upload session was not found .... re-create session\");}\n\t\t\t\t\tnewUploadSession = createSessionForFileUpload(\n\t\t\t\t\t\tactiveOneDriveApiInstance, \n\t\t\t\t\t\tuploadSessionData[\"localPath\"].str, \n\t\t\t\t\t\tuploadSessionData[\"targetDriveId\"].str, \n\t\t\t\t\t\tuploadSessionData[\"targetParentId\"].str, \n\t\t\t\t\t\tbaseName(uploadSessionData[\"localPath\"].str), \n\t\t\t\t\t\tnull, \n\t\t\t\t\t\tthreadUploadSessionFilePath\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Issue https://github.com/abraunegg/onedrive/issues/2747\n\t\t\t\t// if a 416 uploadResponse is generated, continue\n\t\t\t\tif (exception.httpStatusCode == 416) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Handle transient errors:\n\t\t\t\t//   408 - Request Time Out\n\t\t\t\t//   429 - Too Many Requests\n\t\t\t\t//   503 - Service Unavailable\n\t\t\t\t//   504 - Gateway Timeout\n\n\t\t\t\t// Insert a new line as well, so that the below error is inserted on the console in the right location\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Fragment upload failed - received an exception response from OneDrive API\", [\"verbose\"]);}\n\n\t\t\t\t// display what the error is if we have not already continued\n\t\t\t\tif (exception.httpStatusCode != 404) {\n\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t}\n\n\t\t\t\t// retry fragment upload in case error is transient\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Retrying fragment upload\", [\"verbose\"]);}\n\n\t\t\t\t// Retry fragment upload logic\n\t\t\t\ttry {\n\t\t\t\t\tstring effectiveRetryUploadURL;\n\t\t\t\t\tstring effectiveLocalPath;\n\n\t\t\t\t\t// If we re-created the session, use the new data on re-try\n\t\t\t\t\tif (newUploadSession.type() == JSONType.object) {\n\t\t\t\t\t\tif (\"uploadUrl\" in newUploadSession) {\n\t\t\t\t\t\t\t// get this from 'newUploadSession'\n\t\t\t\t\t\t\teffectiveRetryUploadURL = newUploadSession[\"uploadUrl\"].str;\n\t\t\t\t\t\t\teffectiveLocalPath = newUploadSession[\"localPath\"].str;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// get this from the original input\n\t\t\t\t\t\t\teffectiveRetryUploadURL = uploadSessionData[\"uploadUrl\"].str;\n\t\t\t\t\t\t\teffectiveLocalPath = uploadSessionData[\"localPath\"].str;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// retry the fragment upload\n\t\t\t\t\t\tuploadResponse = activeOneDriveApiInstance.uploadFragment(\n\t\t\t\t\t\t\teffectiveRetryUploadURL,\n\t\t\t\t\t\t\teffectiveLocalPath,\n\t\t\t\t\t\t\toffset,\n\t\t\t\t\t\t\tfragSize,\n\t\t\t\t\t\t\tthisFileSize\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// newUploadSession not a JSON\n\t\t\t\t\t\tuploadResponse = null;\n\t\t\t\t\t\treturn uploadResponse;\n\t\t\t\t\t}\n\t\t\t\t} catch (OneDriveException e) {\n\t\t\t\t\t// OneDrive threw another error on retry\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Retry to upload fragment failed\", [\"verbose\"]);}\n\t\t\t\t\t// display what the error is\n\t\t\t\t\tdisplayOneDriveErrorMessage(e.msg, thisFunctionName);\n\t\t\t\t\t// set uploadResponse to null as the fragment upload was in error twice\n\t\t\t\t\tuploadResponse = null;\n\t\t\t\t\t\n\t\t\t\t} catch (std.exception.ErrnoException e) {\n\t\t\t\t\t// There was a file system error - display the error message\n\t\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, newUploadSession[\"localPath\"].str);\n\t\t\t\t\treturn uploadResponse;\n\t\t\t\t}\n\t\t\t} catch (ErrnoException e) {\n\t\t\t\t// There was a file system error\n\t\t\t\t// display the error message\n\t\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, uploadSessionData[\"localPath\"].str);\n\t\t\t\tuploadResponse = null;\n\t\t\t\treturn uploadResponse;\n\t\t\t}\n\n\t\t\t// was the fragment uploaded without issue?\n\t\t\tif (uploadResponse.type() == JSONType.object) {\n\t\t\t\t// Fragment uploaded\n\t\t\t\tif (debugLogging) {addLogEntry(\"Fragment upload complete\", [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Use updated offset from response, not fixed increment\n\t\t\t\tif (\"nextExpectedRanges\" in uploadResponse &&\n\t\t\t\t\tuploadResponse[\"nextExpectedRanges\"].type() == JSONType.array &&\n\t\t\t\t\t!uploadResponse[\"nextExpectedRanges\"].array.empty) {\n\t\t\t\t\toffset = uploadResponse[\"nextExpectedRanges\"].array[0].str.splitter('-').front.to!long;\n\t\t\t\t} else {\n\t\t\t\t\t// No nextExpectedRanges? Assume upload complete\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t// update the uploadSessionData details\n\t\t\t\tuploadSessionData[\"expirationDateTime\"] = uploadResponse[\"expirationDateTime\"];\n\t\t\t\tuploadSessionData[\"nextExpectedRanges\"] = uploadResponse[\"nextExpectedRanges\"];\n\t\t\t\t\n\t\t\t\t// Log URL 'updated' expirationDateTime as 'UTC' and 'localTime'\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\t// Convert expiration time to localTime\n\t\t\t\t\tstring utcExpiry = uploadResponse[\"expirationDateTime\"].str;\n\t\t\t\t\tSysTime expiryUTC = SysTime.fromISOExtString(utcExpiry);\n\t\t\t\t\tSysTime expiryLocal = expiryUTC.toLocalTime();\n\t\t\t\t\n\t\t\t\t\t// Display updated URL expiry as UTC and localTime\n\t\t\t\t\taddLogEntry(\"Upload Session URL expiration extended to (UTC):   \" ~ to!string(expiryUTC), [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"Upload Session URL expiration extended to (Local): \" ~ to!string(expiryLocal), [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"\", [\"debug\"]); // Add new line as this fragment is complete\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Save for reuse\n\t\t\t\tsaveSessionFile(threadUploadSessionFilePath, uploadSessionData);\n\t\t\t} else {\n\t\t\t\t// not a JSON object - fragment upload failed\n\t\t\t\tif (verboseLogging) {addLogEntry(\"File upload session failed - invalid response from OneDrive API\", [\"verbose\"]);}\n\n\t\t\t\t// cleanup session data\n\t\t\t\tif (exists(threadUploadSessionFilePath)) {\n\t\t\t\t\tsafeRemove(threadUploadSessionFilePath);\n\t\t\t\t}\n\t\t\t\t// set uploadResponse to null as error\n\t\t\t\tuploadResponse = null;\n\t\t\t\treturn uploadResponse;\n\t\t\t}\n\t\t}\n\n\t\t// Upload complete\n\t\tlong end_unix_time = Clock.currTime.toUnixTime();\n\t\tauto upload_duration = cast(int)(end_unix_time - start_unix_time);\n\t\tdur!\"seconds\"(upload_duration).split!(\"hours\", \"minutes\", \"seconds\")(h, m, s);\n\t\tetaString = format!\"| DONE in %02d:%02d:%02d\"(h, m, s);\n\t\taddLogEntry(uploadLogEntry ~ \"100% \" ~ etaString, [\"consoleOnly\"]);\n\n\t\t// Remove session file if it exists\t\t\n\t\tif (exists(threadUploadSessionFilePath)) {\n\t\t\tsafeRemove(threadUploadSessionFilePath);\n\t\t}\n\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\n\t\t// Return the session upload response\n\t\treturn uploadResponse;\n\t}\n\n\t\n\t// Delete an item on OneDrive\n\tvoid uploadDeletedItem(Item itemToDelete, string path) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tOneDriveApi uploadDeletedItemOneDriveApiInstance;\n\t\t\t\n\t\t// Are we in a situation where we HAVE to keep the data online - do not delete the remote object\n\t\tif (noRemoteDelete) {\n\t\t\tif ((itemToDelete.type == ItemType.dir)) {\n\t\t\t\t// Do not process remote directory delete\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping remote directory delete as --upload-only & --no-remote-delete configured\", [\"verbose\"]);}\n\t\t\t} else {\n\t\t\t\t// Do not process remote file delete\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping remote file delete as --upload-only & --no-remote-delete configured\", [\"verbose\"]);}\n\t\t\t}\n\t\t} else {\n\t\t\t\n\t\t\t// Is this a --download-only operation?\n\t\t\tif (!appConfig.getValueBool(\"download_only\")) {\n\t\t\t\t// Process the delete - delete the object online\n\t\t\t\taddLogEntry(\"Deleting item from Microsoft OneDrive: \" ~ path, fileTransferNotifications());\n\t\t\t\tbool flagAsBigDelete = false;\n\t\t\t\t\n\t\t\t\tItem[] children;\n\t\t\t\tlong itemsToDelete;\n\t\t\t\n\t\t\t\tif ((itemToDelete.type == ItemType.dir)) {\n\t\t\t\t\t// Query the database - how many objects will this remove?\n\t\t\t\t\tchildren = getChildren(itemToDelete.driveId, itemToDelete.id);\n\t\t\t\t\t// Count the returned items + the original item (1)\n\t\t\t\t\titemsToDelete = count(children) + 1;\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Number of items online to delete: \" ~ to!string(itemsToDelete), [\"debug\"]);}\n\t\t\t\t} else {\n\t\t\t\t\titemsToDelete = 1;\n\t\t\t\t}\n\t\t\t\t// Clear array\n\t\t\t\tchildren = [];\n\t\t\t\t\n\t\t\t\t// 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\n\t\t\t\t// The user may also be --sync process, so we are checking if something was deleted between application use\n\t\t\t\tif (itemsToDelete >= appConfig.getValueLong(\"classify_as_big_delete\")) {\n\t\t\t\t\t// A big delete has been detected\n\t\t\t\t\tflagAsBigDelete = true;\n\t\t\t\t\tif (!appConfig.getValueBool(\"force\")) {\n\t\t\t\t\t\t// Send this message to the GUI\n\t\t\t\t\t\taddLogEntry(\"ERROR: An attempt to remove a large volume of data from OneDrive has been detected. Exiting client to preserve your data on Microsoft OneDrive\", [\"info\", \"notify\"]);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Additional application logging\n\t\t\t\t\t\taddLogEntry(\"ERROR: The total number of items being deleted is: \" ~ to!string(itemsToDelete));\n\t\t\t\t\t\taddLogEntry(\"ERROR: To delete a large volume of data use --force or increase the config value 'classify_as_big_delete' to a larger value\");\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Must exit here to preserve data on online , allow logging to be done\n\t\t\t\t\t\tforceExit();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Are we in a --dry-run scenario?\n\t\t\t\tif (!dryRun) {\n\t\t\t\t\t// We are not in a dry run scenario\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"itemToDelete: \" ~ to!string(itemToDelete), [\"debug\"]);\n\t\t\t\t\t\t// what item are we trying to delete?\n\t\t\t\t\t\taddLogEntry(\"Attempting to delete this single item id: \" ~ itemToDelete.id ~ \" from drive: \" ~ itemToDelete.driveId, [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Configure these item variables to handle OneDrive Business Shared Folder Deletion\n\t\t\t\t\tItem actualItemToDelete;\n\t\t\t\t\tItem remoteShortcutLinkItem;\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive Shared Folder Link Handling\n\t\t\t\t\t// - If the item to delete is on a remote drive ... technically we do not own this and should not be deleting this online\n\t\t\t\t\t//   We should however be deleting the 'link' in our account online, and, remove the DB link entries (root / folder DB Tie records)\n\t\t\t\t\tbool businessSharingEnabled = false;\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive Business Shared Folder Deletion Handling\n\t\t\t\t\t// Is this a Business Account with Sync Business Shared Items enabled?\n\t\t\t\t\tif ((appConfig.accountType == \"business\") && (appConfig.getValueBool(\"sync_business_shared_items\"))) {\n\t\t\t\t\t\t// Syncing Business Shared Items is enabled\n\t\t\t\t\t\tbusinessSharingEnabled = true;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Is this a 'personal' account type or is this a Business Account with Sync Business Shared Items enabled?\n\t\t\t\t\tif ((appConfig.accountType == \"personal\") || businessSharingEnabled) {\n\t\t\t\t\t\t// Personal account type or syncing Business Shared Items is enabled\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\titemToDelete.driveId = transformToLowerCase(itemToDelete.driveId);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is the 'drive' where this is to be deleted on 'our' drive or is this a remote 'drive' ?\n\t\t\t\t\t\tif (itemToDelete.driveId != appConfig.defaultDriveId) {\n\t\t\t\t\t\t\t// The item to delete is on a remote drive ... this must be handled in a specific way\n\t\t\t\t\t\t\tif (itemToDelete.type == ItemType.dir) {\n\t\t\t\t\t\t\t\t// Select the 'remote' database object type using these details\n\t\t\t\t\t\t\t\t// Get the DB entry for this 'remote' item\n\t\t\t\t\t\t\t\titemDB.selectRemoteTypeByRemoteDriveId(itemToDelete.driveId, itemToDelete.id, remoteShortcutLinkItem);\n\t\t\t\t\t\t\t}\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// We potentially now have the correct details to delete in our account\n\t\t\t\t\t\tif (remoteShortcutLinkItem.type == ItemType.remote) {\n\t\t\t\t\t\t\t// A valid 'remote' DB entry was returned\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"remoteShortcutLinkItem: \" ~ to!string(remoteShortcutLinkItem), [\"debug\"]);}\n\t\t\t\t\t\t\t// Set actualItemToDelete to this data\n\t\t\t\t\t\t\tactualItemToDelete = remoteShortcutLinkItem;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Delete the shortcut reference in the local database\n\t\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t\t// Personal Shared Folder deletion message\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Deleted OneDrive Personal Shared Folder 'Shortcut Link'\", [\"debug\"]);}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Business Shared Folder deletion message\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Deleted OneDrive Business Shared Folder 'Shortcut Link'\", [\"debug\"]);}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Perform action deletion from database\n\t\t\t\t\t\t\titemDB.deleteById(remoteShortcutLinkItem.driveId, remoteShortcutLinkItem.id);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// No data was returned, use the original data\n\t\t\t\t\t\t\tactualItemToDelete = itemToDelete;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Set actualItemToDelete to original data\n\t\t\t\t\t\tactualItemToDelete = itemToDelete;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Try the online deletion using the 'actualItemToDelete' values\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Create new OneDrive API Instance\n\t\t\t\t\t\tuploadDeletedItemOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t\t\t\t\tuploadDeletedItemOneDriveApiInstance.initialise();\n\t\t\t\t\t\n\t\t\t\t\t\tif (!permanentDelete) {\n\t\t\t\t\t\t\t// Perform the delete via the default OneDrive API instance\n\t\t\t\t\t\t\tuploadDeletedItemOneDriveApiInstance.deleteById(actualItemToDelete.driveId, actualItemToDelete.id);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Perform the permanent delete via the default OneDrive API instance\n\t\t\t\t\t\t\tuploadDeletedItemOneDriveApiInstance.permanentDeleteById(actualItemToDelete.driveId, actualItemToDelete.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\tuploadDeletedItemOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\tuploadDeletedItemOneDriveApiInstance = null;\n\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\n\t\t\t\t\t} catch (OneDriveException e) {\n\t\t\t\t\t\tif (e.httpStatusCode == 404) {\n\t\t\t\t\t\t\t// item.id, item.eTag could not be found on the specified driveId\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"OneDrive reported: The resource could not be found to be deleted.\", [\"verbose\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\tuploadDeletedItemOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\tuploadDeletedItemOneDriveApiInstance = null;\n\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Delete the reference in the local database - use the original input\n\t\t\t\t\titemDB.deleteById(itemToDelete.driveId, itemToDelete.id);\n\t\t\t\t\t\n\t\t\t\t\t// Was the original item a 'Shared Folder' ?\n\t\t\t\t\tif (remoteShortcutLinkItem.type == ItemType.remote) {\n\t\t\t\t\t\t// Are there any other 'children' for itemToDelete parent ... this parent may have other Shared Folders added to our account that we have not removed ..\n\t\t\t\t\t\tItem[] remainingChildren;\n\t\t\t\t\t\tremainingChildren ~= itemDB.selectChildren(itemToDelete.driveId, itemToDelete.parentId);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Only if there are zero children for this parent item, remove the 'root' record\n\t\t\t\t\t\tif (count(remainingChildren) == 0) {\n\t\t\t\t\t\t\t// No more children for this parental object\n\t\t\t\t\t\t\titemDB.deleteById(itemToDelete.driveId, itemToDelete.parentId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// log that this is a dry-run activity\n\t\t\t\t\taddLogEntry(\"DRY-RUN: No delete activity\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// --download-only operation, we are not uploading any delete event to OneDrive\n\t\t\t\tif (debugLogging) {addLogEntry(\"Not pushing local delete to Microsoft OneDrive due to --download-only being used\", [\"debug\"]);}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Get the children of an item id from the database\n\tItem[] getChildren(string driveId, string id) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\tItem[] children;\n\t\tchildren ~= itemDB.selectChildren(driveId, id);\n\t\tforeach (Item child; children) {\n\t\t\tif (child.type != ItemType.file) {\n\t\t\t\t// recursively get the children of this child\n\t\t\t\tchildren ~= getChildren(child.driveId, child.id);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return the database records\n\t\treturn children;\n\t}\n\t\n\t// Perform a 'reverse' delete of all child objects on OneDrive\n\tvoid performReverseDeletionOfOneDriveItems(Item[] children, Item itemToDelete) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Log what is happening\n\t\tif (debugLogging) {addLogEntry(\"Attempting a reverse delete of all child objects from OneDrive\", [\"debug\"]);}\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi performReverseDeletionOneDriveApiInstance;\n\t\tperformReverseDeletionOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tperformReverseDeletionOneDriveApiInstance.initialise();\n\t\t\n\t\tforeach_reverse (Item child; children) {\n\t\t\t// Log the action\n\t\t\tif (debugLogging) {addLogEntry(\"Attempting to delete this child item id: \" ~ child.id ~ \" from drive: \" ~ child.driveId, [\"debug\"]);}\n\t\t\t\n\t\t\tif (!permanentDelete) {\n\t\t\t\t// Perform the delete via the default OneDrive API instance\n\t\t\t\tperformReverseDeletionOneDriveApiInstance.deleteById(child.driveId, child.id, child.eTag);\n\t\t\t} else {\n\t\t\t\t// Perform the permanent delete via the default OneDrive API instance\n\t\t\t\tperformReverseDeletionOneDriveApiInstance.permanentDeleteById(child.driveId, child.id, child.eTag);\n\t\t\t}\n\t\t\t\n\t\t\t// delete the child reference in the local database\n\t\t\titemDB.deleteById(child.driveId, child.id);\n\t\t}\n\t\t// Log the action\n\t\tif (debugLogging) {addLogEntry(\"Attempting to delete this parent item id: \" ~ itemToDelete.id ~ \" from drive: \" ~ itemToDelete.driveId, [\"debug\"]);}\n\t\t\n\t\tif (!permanentDelete) {\n\t\t\t// Perform the delete via the default OneDrive API instance\n\t\t\tperformReverseDeletionOneDriveApiInstance.deleteById(itemToDelete.driveId, itemToDelete.id, itemToDelete.eTag);\n\t\t} else {\n\t\t\t// Perform the permanent delete via the default OneDrive API instance\n\t\t\tperformReverseDeletionOneDriveApiInstance.permanentDeleteById(itemToDelete.driveId, itemToDelete.id, itemToDelete.eTag);\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tperformReverseDeletionOneDriveApiInstance.releaseCurlEngine();\n\t\tperformReverseDeletionOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Create a fake OneDrive response suitable for use with saveItem\n\tJSONValue createFakeResponse(string path) {\n\t\timport std.digest.sha;\n\t\t\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Generate a simulated JSON response which can be used\n\t\t// At a minimum we need:\n\t\t// 1. eTag\n\t\t// 2. cTag\n\t\t// 3. fileSystemInfo\n\t\t// 4. file or folder. if file, hash of file\n\t\t// 5. id\n\t\t// 6. name\n\t\t// 7. parent reference\n\t\t\n\t\tstring fakeDriveId = appConfig.defaultDriveId;\n\t\tstring fakeRootId = appConfig.defaultRootId;\n\t\tSysTime mtime = exists(path) ? timeLastModified(path).toUTC() : Clock.currTime(UTC());\n\t\tauto sha1 = new SHA1Digest();\n\t\tubyte[] fakedOneDriveItemValues = sha1.digest(path);\n\t\tJSONValue fakeResponse;\n\n\t\tstring parentPath = dirName(path);\n\t\tif (parentPath != \".\" && exists(path)) {\n\t\t\tforeach (searchDriveId; onlineDriveDetails.keys) {\n\t\t\t\tItem databaseItem;\n\t\t\t\tif (itemDB.selectByPath(parentPath, searchDriveId, databaseItem)) {\n\t\t\t\t\tfakeDriveId = databaseItem.driveId;\n\t\t\t\t\tfakeRootId = databaseItem.id;\n\t\t\t\t\tbreak; // Exit loop after finding the first match\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfakeResponse = [\n\t\t\t\"id\": JSONValue(toHexString(fakedOneDriveItemValues)),\n\t\t\t\"cTag\": JSONValue(toHexString(fakedOneDriveItemValues)),\n\t\t\t\"eTag\": JSONValue(toHexString(fakedOneDriveItemValues)),\n\t\t\t\"fileSystemInfo\": JSONValue([\n\t\t\t\t\"createdDateTime\": mtime.toISOExtString(),\n\t\t\t\t\"lastModifiedDateTime\": mtime.toISOExtString()\n\t\t\t]),\n\t\t\t\"name\": JSONValue(baseName(path)),\n\t\t\t\"parentReference\": JSONValue([\n\t\t\t\t\"driveId\": JSONValue(fakeDriveId),\n\t\t\t\t\"driveType\": JSONValue(appConfig.accountType),\n\t\t\t\t\"id\": JSONValue(fakeRootId)\n\t\t\t])\n\t\t];\n\n\t\tif (exists(path)) {\n\t\t\tif (isDir(path)) {\n\t\t\t\tfakeResponse[\"folder\"] = JSONValue(\"\");\n\t\t\t} else {\n\t\t\t\tstring quickXorHash = computeQuickXorHash(path);\n\t\t\t\tfakeResponse[\"file\"] = JSONValue([\n\t\t\t\t\t\"hashes\": JSONValue([\"quickXorHash\": JSONValue(quickXorHash)])\n\t\t\t\t]);\n\t\t\t}\n\t\t} else {\n\t\t\t// Assume directory if path does not exist\n\t\t\tfakeResponse[\"folder\"] = JSONValue(\"\");\n\t\t}\n\n\t\tif (debugLogging) {addLogEntry(\"Generated Fake OneDrive Response: \" ~ to!string(fakeResponse), [\"debug\"]);}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return the generated fake API response\n\t\treturn fakeResponse;\n\t}\n\n\t// Save JSON item details into the item database\n\tvoid saveItem(JSONValue jsonItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// jsonItem has to be a valid object\n\t\tif (jsonItem.type() == JSONType.object) {\n\t\t\n\t\t\t// Issue #3336 - Convert driveId to lowercase\n\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t// We must massage this raw JSON record to force the jsonItem[\"parentReference\"][\"driveId\"] to lowercase\n\t\t\t\tif (hasParentReferenceDriveId(jsonItem)) {\n\t\t\t\t\t// This JSON record has a driveId we now must manipulate to lowercase\n\t\t\t\t\tstring originalDriveIdValue = jsonItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tjsonItem[\"parentReference\"][\"driveId\"] = transformToLowerCase(originalDriveIdValue);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Check if the response JSON has an 'id', otherwise makeItem() fails with 'Key not found: id'\n\t\t\tif (hasId(jsonItem)) {\n\t\t\t\t// Are we in a --upload-only & --remove-source-files scenario?\n\t\t\t\t// 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\n\t\t\t\t// 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\n\t\t\t\tif ((uploadOnly) && (localDeleteAfterUpload) && (isItemFile(jsonItem))) {\n\t\t\t\t\t// Log that we skipping adding item to the local DB and the reason why\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Skipping adding to database as --upload-only & --remove-source-files configured\", [\"debug\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// Takes a JSON input and formats to an item which can be used by the database\n\t\t\t\t\tItem item = makeItem(jsonItem);\n\t\t\t\t\t\n\t\t\t\t\t// Is this JSON item a 'root' item?\n\t\t\t\t\tif ((isItemRoot(jsonItem)) && (item.name == \"root\")) {\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"Creating 'root' DB item from this JSON: \" ~ sanitiseJSONItem(jsonItem), [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"Updating DB Item object with correct values as this is a 'root' object\", [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\" item.parentId = null\", [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\" item.type = ItemType.root\", [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\titem.parentId = null; \t// ensures that this database entry has no parent\n\t\t\t\t\t\titem.type = ItemType.root;\n\t\t\t\t\t\t// Check for parentReference\n\t\t\t\t\t\tif (hasParentReference(jsonItem)) {\n\t\t\t\t\t\t\t// Set the correct item.driveId\n\t\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\t\taddLogEntry(\"The 'root' JSON Item HAS a parentReference .... setting item.driveId = jsonItem['parentReference']['driveId'].str from the provided JSON record\", [\"debug\"]);\n\t\t\t\t\t\t\t\tstring logMessage = format(\" item.driveId = '%s'\", jsonItem[\"parentReference\"][\"driveId\"].str);\n\t\t\t\t\t\t\t\taddLogEntry(logMessage, [\"debug\"]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\titem.driveId = jsonItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\t\t\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Issue #3115 - Validate driveId length\n\t\t\t\t\t\t// What account type is this?\n\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\t\t\t\titem.driveId = transformToLowerCase(item.driveId);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\t\t\t\t\tif (item.driveId != appConfig.defaultDriveId) {\n\t\t\t\t\t\t\t\titem.driveId = testProvidedDriveIdForLengthIssue(item.driveId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// We only should be adding our account 'root' to the database, not shared folder 'root' items\n\t\t\t\t\t\tif (item.driveId != appConfig.defaultDriveId) {\n\t\t\t\t\t\t\t// Shared Folder drive 'root' object .. we dont want this item\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"NOT adding 'remote root' object to database: \" ~ to!string(item), [\"debug\"]);}\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Issue #3115 - Validate driveId length\n\t\t\t\t\t// What account type is this?\n\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\t\t\titem.driveId = transformToLowerCase(item.driveId);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\t\t\t\tif (item.driveId != appConfig.defaultDriveId) {\n\t\t\t\t\t\t\titem.driveId = testProvidedDriveIdForLengthIssue(item.driveId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Add to the local database\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Saving this DB item record: \" ~ to!string(item), [\"debug\"]);}\n\t\t\t\t\titemDB.upsert(item);\n\t\t\t\t\t\n\t\t\t\t\t// If we have a remote drive ID, add this to our list of known drive id's\n\t\t\t\t\tif (!item.remoteDriveId.empty) {\n\t\t\t\t\t\t// Keep the DriveDetailsCache array with unique entries only\n\t\t\t\t\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\t\t\t\t\tif (!canFindDriveId(item.remoteDriveId, cachedOnlineDriveData)) {\n\t\t\t\t\t\t\t// Add this driveId to the drive cache\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Database item is a remote drive object, need to fetch online details for this drive: \" ~ to!string(item.remoteDriveId), [\"debug\"]);}\n\t\t\t\t\t\t\taddOrUpdateOneDriveOnlineDetails(item.remoteDriveId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// log error\n\t\t\t\taddLogEntry(\"ERROR: OneDrive response missing required 'id' element\");\n\t\t\t\taddLogEntry(\"ERROR: \" ~ sanitiseJSONItem(jsonItem));\n\t\t\t}\n\t\t} else {\n\t\t\t// Log that the provided JSON could not be processed\n\t\t\taddLogEntry(\"ERROR: Invalid JSON object - the provided data cannot be processed or stored in the database.\");\n\t\t\t\n\t\t\t// What level of next message is provided?\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t// Standard error message\n\t\t\t\taddLogEntry(\"ERROR: Please rerun the application with --verbose enabled to obtain additional diagnostic information.\");\n\t\t\t} else {\n\t\t\t\t// verbose or debug\n\t\t\t\taddLogEntry(\"ERROR: The following JSON data failed validation and could not be saved: \" ~ to!string(jsonItem));\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Save an already created database object into the database\n\tvoid saveDatabaseItem(Item newDatabaseItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Issue #3115 - Personal Account Shared Folder\n\t\t// What account type is this?\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Issue #3336 - Convert driveId to lowercase for the DB record\n\t\t\tstring actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(newDatabaseItem.driveId));\n\t\t\tnewDatabaseItem.driveId = actualOnlineDriveId;\n\t\t\t\n\t\t\t// Is this a 'remote' DB record\n\t\t\tif (newDatabaseItem.type == ItemType.remote) {\n\t\t\t\t// Issue #3336 - Convert remoteDriveId to lowercase before any test\n\t\t\t\tnewDatabaseItem.remoteDriveId = transformToLowerCase(newDatabaseItem.remoteDriveId);\n\t\t\t\n\t\t\t\t// Test remoteDriveId length and validation if the remoteDriveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\t\tif (newDatabaseItem.remoteDriveId != appConfig.defaultDriveId) {\n\t\t\t\t\t// Issue #3136, #3139 #3143\n\t\t\t\t\t// Fetch the actual online record for this item\n\t\t\t\t\t// This returns the actual OneDrive Personal remoteDriveId value and is 15 character checked\n\t\t\t\t\tstring actualOnlineRemoteDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(newDatabaseItem.remoteDriveId));\n\t\t\t\t\tnewDatabaseItem.remoteDriveId = actualOnlineRemoteDriveId;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Add the database record\n\t\tif (debugLogging) {addLogEntry(\"Creating a new database record for a new local path that has been created: \" ~ to!string(newDatabaseItem), [\"debug\"]);}\n\t\titemDB.upsert(newDatabaseItem);\n\t\t\n\t\t// If we have a remote drive ID, add this to our list of known drive id's\n\t\tif (!newDatabaseItem.remoteDriveId.empty) {\n\t\t\t// Keep the DriveDetailsCache array with unique entries only\n\t\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\t\tif (!canFindDriveId(newDatabaseItem.remoteDriveId, cachedOnlineDriveData)) {\n\t\t\t\t// Add this driveId to the drive cache\n\t\t\t\tif (debugLogging) {addLogEntry(\"New database record is a remote drive object, need to fetch online details for this drive: \" ~ to!string(newDatabaseItem.remoteDriveId), [\"debug\"]);}\n\t\t\t\taddOrUpdateOneDriveOnlineDetails(newDatabaseItem.remoteDriveId);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Wrapper function for makeDatabaseItem so we can check to ensure that the item has the required hashes\n\tItem makeItem(JSONValue onedriveJSONItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\n\t\t// Make the DB Item from the JSON data provided\n\t\tItem newDatabaseItem = makeDatabaseItem(onedriveJSONItem);\n\t\t\n\t\t// Is this a 'file' item that has not been deleted? Deleted items have no hash\n\t\tif ((newDatabaseItem.type == ItemType.file) && (!isItemDeleted(onedriveJSONItem))) {\n\t\t\t// Does this item have a file size attribute?\n\t\t\tif (hasFileSize(onedriveJSONItem)) {\n\t\t\t\t// Is the file size greater than 0?\n\t\t\t\tif (onedriveJSONItem[\"size\"].integer > 0) {\n\t\t\t\t\t// Does the DB item have any hashes as per the API provided JSON data?\n\t\t\t\t\tif ((newDatabaseItem.quickXorHash.empty) && (newDatabaseItem.sha256Hash.empty)) {\n\t\t\t\t\t\t// Odd .. there is no hash for this item .. why is that?\n\t\t\t\t\t\t// Is there a 'file' JSON element?\n\t\t\t\t\t\tif (\"file\" in onedriveJSONItem) {\n\t\t\t\t\t\t\t// Microsoft OneDrive OneNote objects will report as files but have 'application/msonenote' and 'application/octet-stream' as mime types\n\t\t\t\t\t\t\tif ((isMicrosoftOneNoteMimeType1(onedriveJSONItem)) || (isMicrosoftOneNoteMimeType2(onedriveJSONItem))) {\n\t\t\t\t\t\t\t\t// Debug log output that this is a potential OneNote object\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"This item is potentially an associated Microsoft OneNote Object Item\", [\"debug\"]);}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Not a Microsoft OneNote Mime Type Object ..\n\t\t\t\t\t\t\t\tstring apiWarningMessage = \"WARNING: OneDrive API inconsistency - this file does not have any hash: \";\n\t\t\t\t\t\t\t\t// This is computationally expensive .. but we are only doing this if there are no hashes provided\n\t\t\t\t\t\t\t\tbool parentInDatabase = itemDB.idInLocalDatabase(newDatabaseItem.driveId, newDatabaseItem.parentId);\n\t\t\t\t\t\t\t\t// Is the parent id in the database?\n\t\t\t\t\t\t\t\tif (parentInDatabase) {\n\t\t\t\t\t\t\t\t\t// This is again computationally expensive .. calculate this item path to advise the user the actual path of this item that has no hash\n\t\t\t\t\t\t\t\t\tstring newItemPath = computeItemPath(newDatabaseItem.driveId, newDatabaseItem.parentId) ~ \"/\" ~ newDatabaseItem.name;\n\t\t\t\t\t\t\t\t\taddLogEntry(apiWarningMessage ~ newItemPath);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Parent is not in the database .. why?\n\t\t\t\t\t\t\t\t\t// Check if the parent item had been skipped .. \n\t\t\t\t\t\t\t\t\tif (newDatabaseItem.parentId in skippedItems) {\n\t\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(apiWarningMessage ~ \"newDatabaseItem.parentId listed within skippedItems\", [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// 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\n\t\t\t\t\t\t\t\t\t\taddLogEntry(apiWarningMessage ~ newDatabaseItem.id);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\t\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// zero file size\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"This item file is zero size - potentially no hash provided by the OneDrive API\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// OneDrive Personal Account driveId and remoteDriveId length check\n\t\t// 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')\n\t\t// - driveId\n\t\t// - remoteDriveId\n\t\t// \n\t\t// Example:\n\t\t//   024470056F5C3E43 (driveId)\n\t\t//   24470056f5c3e43  (remoteDriveId)\n\t\t// If this is a OneDrive Personal Account, ensure this value is 16 characters, padded by leading zero's if eventually required\n\t\t// What account type is this?\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Check the newDatabaseItem.remoteDriveId\n\t\t\tif (!newDatabaseItem.remoteDriveId.empty) {\n\t\t\t\t// Issue #3136, #3139 #3143\n\t\t\t\t// Test searchItem.driveId length and validation\n\t\t\t\t// - This check the length, fetch online value and return a 16 character driveId\n\t\t\t\tnewDatabaseItem.remoteDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(newDatabaseItem.remoteDriveId));\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return the new database item\n\t\treturn newDatabaseItem;\n\t}\n\t\n\t// For OneDrive Personal Accounts, the case sensitivity depending on the API call means the 'driveId' can be uppercase or lowercase\n\t// For this application use, this causes issues as, in POSIX environments - 024470056F5C3E43 != 024470056f5c3e43 despite on Windows this being treated as the same\n\t// This function does NOT do a 15 character driveId validation\n\tstring fetchRealOnlineDriveIdentifier(string inputDriveId) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// What are we doing\n\t\tif (debugLogging) {\n\t\t\tstring fetchRealValueLogMessage = format(\"Fetching actual online 'driveId' value for '%s'\", inputDriveId);\n\t\t\taddLogEntry(fetchRealValueLogMessage, [\"debug\"]);\n\t\t}\n\t\n\t\t// variables for this function\n\t\tJSONValue remoteDriveDetails;\n\t\tOneDriveApi fetchDriveDetailsOneDriveApiInstance;\n\t\tstring outputDriveId;\n\t\t\n\t\t// Create new OneDrive API Instance\n\t\tfetchDriveDetailsOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tfetchDriveDetailsOneDriveApiInstance.initialise();\n\t\t\n\t\t// Get root details for the provided driveId\n\t\ttry {\n\t\t\tremoteDriveDetails = fetchDriveDetailsOneDriveApiInstance.getDriveIdRoot(inputDriveId);\n\t\t} catch (OneDriveException exception) {\n\t\t\tif (debugLogging) {addLogEntry(\"remoteDriveDetails = fetchDriveDetailsOneDriveApiInstance.getDriveIdRoot(inputDriveId) generated a OneDriveException\", [\"debug\"]);}\n\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t// Display what the error is\n\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tfetchDriveDetailsOneDriveApiInstance.releaseCurlEngine();\n\t\tfetchDriveDetailsOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Do we have details we can use?\n\t\tif (hasParentReferenceDriveId(remoteDriveDetails)) {\n\t\t\t// We have a [parentReference][driveId] reference driveId to use\n\t\t\toutputDriveId = remoteDriveDetails[\"parentReference\"][\"driveId\"].str;\n\t\t} else {\n\t\t\t// We dont have a value from online we can use\n\t\t\t// Test existing driveId length and validation\n\t\t\toutputDriveId = inputDriveId;\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return the outputDriveId\n\t\treturn outputDriveId;\n\t}\n\t\n\t// Print the fileDownloadFailures and fileUploadFailures arrays if they are not empty\n\tvoid displaySyncFailures() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\tbool logFailures(string[] failures, string operation) {\n\t\t\tif (failures.empty) return false;\n\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"Failed items to \" ~ operation ~ \" to/from Microsoft OneDrive: \" ~ to!string(failures.length));\n\n\t\t\tforeach (failedFile; failures) {\n\t\t\t\taddLogEntry(\"Failed to \" ~ operation ~ \": \" ~ failedFile, [\"info\", \"notify\"]);\n\n\t\t\t\tforeach (searchDriveId; onlineDriveDetails.keys) {\n\t\t\t\t\tItem dbItem;\n\t\t\t\t\tif (itemDB.selectByPath(failedFile, searchDriveId, dbItem)) {\n\t\t\t\t\t\taddLogEntry(\"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);\n\t\t\t\t\t\titemDB.deleteById(dbItem.driveId, dbItem.id);\n\t\t\t\t\t\tif (dbItem.remoteDriveId != null) {\n\t\t\t\t\t\t\titemDB.deleteById(dbItem.remoteDriveId, dbItem.remoteId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tbool downloadFailuresLogged = logFailures(fileDownloadFailures, \"download\");\n\t\tbool uploadFailuresLogged = logFailures(fileUploadFailures, \"upload\");\n\t\tsyncFailures = downloadFailuresLogged || uploadFailuresLogged;\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Generate a /delta compatible response - for use when we cant actually use /delta\n\t// This is required when the application is configured to use National Azure AD deployments as these do not support /delta queries\n\t// 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,\n\t// 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\n\t// JSON response which can then be processed, negating the need to continuously traverse the tree and 'exclude' items\n\tJSONValue generateDeltaResponse(string pathToQuery = null) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// JSON value which will be responded with\n\t\tJSONValue selfGeneratedDeltaResponse;\n\t\t\n\t\t// Function variables\n\t\tbool remotePathObject = false;\n\t\tItem searchItem;\n\t\tJSONValue rootData;\n\t\tJSONValue driveData;\n\t\tJSONValue pathData;\n\t\tJSONValue topLevelChildren;\n\t\tJSONValue[] childrenData;\n\t\tstring nextLink;\n\t\tOneDriveApi generateDeltaResponseOneDriveApiInstance;\n\t\t\n\t\t// Was a path to query passed in?\n\t\tif (pathToQuery.empty) {\n\t\t\t// Will query for the 'root'\n\t\t\tpathToQuery = \".\";\n\t\t}\n\t\t\n\t\t// Create new OneDrive API Instance\n\t\tgenerateDeltaResponseOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tgenerateDeltaResponseOneDriveApiInstance.initialise();\n\t\t\n\t\t// Is this a --single-directory invocation?\n\t\tif (!singleDirectoryScope) {\n\t\t\t// In a --resync scenario, there is no DB data to query, so we have to query the OneDrive API here to get relevant details\n\t\t\ttry {\n\t\t\t\t// Query the OneDrive API, using the path, which will query 'our' OneDrive Account\n\t\t\t\tpathData = generateDeltaResponseOneDriveApiInstance.getPathDetails(pathToQuery);\n\t\t\t\t\n\t\t\t\t// Is the path on OneDrive local or remote to our account drive id?\n\t\t\t\tif (!isItemRemote(pathData)) {\n\t\t\t\t\t// The path we are seeking is local to our account drive id\n\t\t\t\t\tsearchItem.driveId = pathData[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tsearchItem.id = pathData[\"id\"].str;\n\t\t\t\t} else {\n\t\t\t\t\t// The path we are seeking is remote to our account drive id\n\t\t\t\t\tsearchItem.driveId = pathData[\"remoteItem\"][\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tsearchItem.id = pathData[\"remoteItem\"][\"id\"].str;\n\t\t\t\t\tremotePathObject = true;\n\t\t\t\t\t\n\t\t\t\t\t// Issue #3115 - Personal Account Shared Folder\n\t\t\t\t\t// What account type is this?\n\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t// Issue #3136, #3139 #3143\n\t\t\t\t\t\t// Fetch the actual online record for this item\n\t\t\t\t\t\t// This returns the actual OneDrive Personal driveId value. The check of 'searchItem.driveId' to comply with 16 characters is done below\n\t\t\t\t\t\tstring actualOnlineDriveId = fetchRealOnlineDriveIdentifier(searchItem.driveId);\n\t\t\t\t\t\tsearchItem.driveId = actualOnlineDriveId;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// Display error message\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tgenerateDeltaResponseOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\tgenerateDeltaResponseOneDriveApiInstance = null;\n\t\t\t\t\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\tforceExit();\n\t\t\t}\n\t\t} else {\n\t\t\t// When setSingleDirectoryScope() was called, the following were set to the correct items, even if the path was remote:\n\t\t\t// - singleDirectoryScopeDriveId\n\t\t\t// - singleDirectoryScopeItemId\n\t\t\t// Reuse these prior set values\n\t\t\tsearchItem.driveId = singleDirectoryScopeDriveId;\n\t\t\tsearchItem.id = singleDirectoryScopeItemId;\n\t\t}\n\t\t\n\t\t// Issue #3072 - Validate searchItem.driveId length\n\t\t// What account type is this?\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\tsearchItem.driveId = transformToLowerCase(searchItem.driveId);\n\t\t\n\t\t\t// Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\tif (searchItem.driveId != appConfig.defaultDriveId) {\n\t\t\t\tsearchItem.driveId = testProvidedDriveIdForLengthIssue(searchItem.driveId);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// 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\n\t\t// Downgrade ONLY files associated with this driveId and idToQuery\n\t\tif (debugLogging) {addLogEntry(\"Downgrading all children for this searchItem.driveId (\" ~ searchItem.driveId ~ \") and searchItem.id (\" ~ searchItem.id ~ \") to an out-of-sync state\", [\"debug\"]);}\n\t\t\n\t\tItem[] drivePathChildren = getChildren(searchItem.driveId, searchItem.id);\n\t\tif (count(drivePathChildren) > 0) {\n\t\t\t// Children to process and flag as out-of-sync\t\n\t\t\tforeach (drivePathChild; drivePathChildren) {\n\t\t\t\t// Flag any object in the database as out-of-sync for this driveId & and object id\n\t\t\t\tif (debugLogging) {addLogEntry(\"Downgrading item as out-of-sync: \" ~ drivePathChild.id, [\"debug\"]);}\n\t\t\t\titemDB.downgradeSyncStatusFlag(drivePathChild.driveId, drivePathChild.id);\n\t\t\t}\n\t\t}\n\t\t// Clear DB response array\n\t\tdrivePathChildren = [];\n\t\t\n\t\t// Get drive details for the provided driveId\n\t\ttry {\n\t\t\tdriveData = generateDeltaResponseOneDriveApiInstance.getPathDetailsById(searchItem.driveId, searchItem.id);\n\t\t} catch (OneDriveException exception) {\n\t\t\t// An error was generated\n\t\t\tif (debugLogging) {addLogEntry(\"driveData = generateDeltaResponseOneDriveApiInstance.getPathDetailsById(searchItem.driveId, searchItem.id) generated a OneDriveException\", [\"debug\"]);}\n\t\t\t\n\t\t\t// Was this a 403 or 404 ?\n\t\t\tif ((exception.httpStatusCode == 403) || (exception.httpStatusCode == 404)) {\n\t\t\t\t// The API call returned a 404 error response\n\t\t\t\tif (debugLogging) {addLogEntry(\"onlineParentData = onlineParentOneDriveApiInstance.getPathDetailsById(parentDriveId, parentObjectId); generated a 404 - shared folder path does not exist online\", [\"debug\"]);}\n\t\t\t\tstring errorMessage = format(\"WARNING: The OneDrive Shared Folder link target '%s' cannot be found online using the provided online data.\", pathToQuery);\n\t\t\t\t// detail what this 404 error response means\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(errorMessage);\n\t\t\t\taddLogEntry(\"WARNING: This is potentially a broken online OneDrive Shared Folder link or you no longer have access to it. Please correct this error online.\");\n\t\t\t\taddLogEntry();\n\t\t\t\t\n\t\t\t\t// Release curl engine\n\t\t\t\tgenerateDeltaResponseOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t// Free object and memory\n\t\t\t\tgenerateDeltaResponseOneDriveApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Return the generated JSON response\n\t\t\t\treturn selfGeneratedDeltaResponse;\n\t\t\t} else {\n\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t// Display what the error is\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Was a valid JSON response for 'driveData' provided?\n\t\tif (driveData.type() == JSONType.object) {\n\t\t\t// Dynamic output for a non-verbose run so that the user knows something is happening\n\t\t\tstring generatingDeltaResponseMessage = format(\"Generating a /delta response from the OneDrive API for this Drive ID: %s and Item ID: %s\", searchItem.driveId, searchItem.id);\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\taddProcessingLogHeaderEntry(generatingDeltaResponseMessage, appConfig.verbosityCount);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (verboseLogging) {addLogEntry(generatingDeltaResponseMessage, [\"verbose\"]);}\n\t\t\t}\n\t\t\n\t\t\t// Process this initial JSON response\n\t\t\tif (!isItemRoot(driveData)) {\n\t\t\t\t// Are we generating a /delta response for a Shared Folder, if not, then we need to add the drive root details first\n\t\t\t\tif (!sharedFolderDeltaGeneration) {\n\t\t\t\t\t// Get root details for the provided driveId\n\t\t\t\t\ttry {\n\t\t\t\t\t\trootData = generateDeltaResponseOneDriveApiInstance.getDriveIdRoot(searchItem.driveId);\n\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"rootData = onedrive.getDriveIdRoot(searchItem.driveId) generated a OneDriveException\", [\"debug\"]);}\n\t\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t}\n\t\t\t\t\t// Add driveData JSON data to array\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Adding OneDrive root details for processing\", [\"verbose\"]);}\n\t\t\t\t\tchildrenData ~= rootData;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Add driveData JSON data to array\n\t\t\tif (verboseLogging) {addLogEntry(\"Adding OneDrive parent folder details for processing\", [\"verbose\"]);}\n\t\t\t\n\t\t\t// What 'driveData' are we adding?\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"Adding this 'driveData' to childrenData = \" ~ to!string(driveData), [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// add the responded 'driveData' to the childrenData to process later\n\t\t\tchildrenData ~= driveData;\n\t\t} else {\n\t\t\t// driveData is an invalid JSON object\n\t\t\taddLogEntry(\"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?\");\n\t\t\t// Release curl engine\n\t\t\tgenerateDeltaResponseOneDriveApiInstance.releaseCurlEngine();\n\t\t\t// Free object and memory\n\t\t\tgenerateDeltaResponseOneDriveApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t\t// Must force exit here, allow logging to be done\n\t\t\tforceExit();\n\t\t}\n\t\t\n\t\t// For each child object, query the OneDrive API\n\t\twhile (true) {\n\t\t\t// Check if exitHandlerTriggered is true\n\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// query top level children\n\t\t\ttry {\n\t\t\t\ttopLevelChildren = generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink);\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// OneDrive threw an error\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(debugLogBreakType1, [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"Query Error: topLevelChildren = generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink)\", [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"driveId:   \" ~ searchItem.driveId, [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"idToQuery: \" ~ searchItem.id, [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"nextLink:  \" ~ nextLink, [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t// Display what the error is\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t\t\n\t\t\t// Process top level children\n\t\t\tif (!remotePathObject) {\n\t\t\t\t// Main account root folder\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Adding \" ~ to!string(count(topLevelChildren[\"value\"].array)) ~ \" OneDrive items for processing from the OneDrive 'root' Folder\", [\"verbose\"]);}\n\t\t\t} else {\n\t\t\t\t// Shared Folder\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Adding \" ~ to!string(count(topLevelChildren[\"value\"].array)) ~ \" OneDrive items for processing from the OneDrive Shared Folder\", [\"verbose\"]);}\n\t\t\t}\n\t\t\t\n\t\t\tforeach (child; topLevelChildren[\"value\"].array) {\n\t\t\t\t// 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.\n\t\t\t\t// This avoids needless calls to the OneDrive API, and potentially speeds up this process.\n\t\t\t\tif (!checkJSONAgainstClientSideFiltering(child)) {\n\t\t\t\t\t// add this child to the array of objects\n\t\t\t\t\tchildrenData ~= child;\n\t\t\t\t\t// is this child a folder?\n\t\t\t\t\tif (isItemFolder(child)) {\n\t\t\t\t\t\t// We have to query this folders children if childCount > 0\n\t\t\t\t\t\tif (child[\"folder\"][\"childCount\"].integer > 0){\n\t\t\t\t\t\t\t// This child folder has children\n\t\t\t\t\t\t\tstring childIdToQuery = child[\"id\"].str;\n\t\t\t\t\t\t\tstring childDriveToQuery = child[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\t\t\tauto childParentPath = child[\"parentReference\"][\"path\"].str.split(\":\");\n\t\t\t\t\t\t\tstring folderPathToScan = childParentPath[1] ~ \"/\" ~ child[\"name\"].str;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tstring pathForLogging;\n\t\t\t\t\t\t\t// 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\n\t\t\t\t\t\t\tif (singleDirectoryScope) {\n\t\t\t\t\t\t\t\tpathForLogging = appConfig.getValueString(\"single_directory\") ~ \"/\" ~ child[\"name\"].str;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tpathForLogging = child[\"name\"].str;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Query the children of this item\n\t\t\t\t\t\t\tJSONValue[] grandChildrenData = queryForChildren(childDriveToQuery, childIdToQuery, folderPathToScan, pathForLogging);\n\t\t\t\t\t\t\tforeach (grandChild; grandChildrenData.array) {\n\t\t\t\t\t\t\t\t// add the grandchild to the array\n\t\t\t\t\t\t\t\tchildrenData ~= grandChild;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// As we are generating a /delta response we need to check if this 'child' JSON is a 'remoteItem' and then handle appropriately\n\t\t\t\t\t// Is this a remote folder JSON ?\n\t\t\t\t\tif (isItemRemote(child)) {\n\t\t\t\t\t\t// Check account type\n\t\t\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\t\t\t// The folder is a remote item ... OneDrive Personal Shared Folder\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"The JSON data indicates this is most likely a OneDrive Personal Shared Folder Link added by 'Add shortcut to My files'\", [\"debug\"]);}\n\t\t\t\t\t\t\t// It is a 'remote' JSON item denoting a potential shared folder\n\t\t\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(child);\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t\tif (appConfig.accountType == \"business\") {\n\t\t\t\t\t\t\t// The folder is a remote item ... OneDrive Business Shared Folder\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"The JSON data indicates this is most likely a OneDrive Shared Business Folder Link added by 'Add shortcut to My files'\", [\"debug\"]);}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Is Shared Business Folder Syncing actually enabled?\n\t\t\t\t\t\t\tif (appConfig.getValueBool(\"sync_business_shared_items\")) {\n\t\t\t\t\t\t\t\t// Shared Business Folder Syncing IS enabled\n\t\t\t\t\t\t\t\t// It is a 'remote' JSON item denoting a potential shared folder\n\t\t\t\t\t\t\t\t// Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner\n\t\t\t\t\t\t\t\tcreateRequiredSharedFolderDatabaseRecords(child);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response \n\t\t\t// to indicate more items are available and provide the request URL for the next page of items.\n\t\t\tif (\"@odata.nextLink\" in topLevelChildren) {\n\t\t\t\t// Update nextLink to next changeSet bundle\n\t\t\t\tif (debugLogging) {addLogEntry(\"Setting nextLink to (@odata.nextLink): \" ~ nextLink, [\"debug\"]);}\n\t\t\t\tnextLink = topLevelChildren[\"@odata.nextLink\"].str;\n\t\t\t} else break;\n\t\t\t\n\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t}\n\t\t\n\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t// Dynamic output for a non-verbose run so that the user knows something is happening\n\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t// Close out the '....' being printed to the console\n\t\t\t\tcompleteProcessingDots();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Craft response from all returned JSON elements\n\t\tselfGeneratedDeltaResponse = [\n\t\t\t\t\t\t\"@odata.context\": JSONValue(\"https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)\"),\n\t\t\t\t\t\t\"value\": JSONValue(childrenData.array)\n\t\t\t\t\t\t];\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tgenerateDeltaResponseOneDriveApiInstance.releaseCurlEngine();\n\t\tgenerateDeltaResponseOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return the generated JSON response\n\t\treturn selfGeneratedDeltaResponse;\n\t}\n\t\n\t// Query the OneDrive API for the specified child id for any children objects\n\tJSONValue[] queryForChildren(string driveId, string idToQuery, string childParentPath, string pathForLogging) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\t\t\n\t\t// function variables\n\t\tJSONValue thisLevelChildren;\n\t\tJSONValue[] thisLevelChildrenData;\n\t\tstring nextLink;\n\t\t\n\t\t// Create new OneDrive API Instance\n\t\tOneDriveApi queryChildrenOneDriveApiInstance;\n\t\tqueryChildrenOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tqueryChildrenOneDriveApiInstance.initialise();\n\t\t\n\t\t// Issue #3115 - Validate driveId length\n\t\t// What account type is this?\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\tdriveId = transformToLowerCase(driveId);\n\t\t\n\t\t\t// Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\tif (driveId != appConfig.defaultDriveId) {\n\t\t\t\tdriveId = testProvidedDriveIdForLengthIssue(driveId);\n\t\t\t}\n\t\t}\n\t\t\n\t\twhile (true) {\n\t\t\t// Check if exitHandlerTriggered is true\n\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t\n\t\t\t// Query this level children\n\t\t\ttry {\n\t\t\t\tthisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance);\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// MAY NEED FUTURE WORK HERE .. YET TO TRIGGER THIS\n\t\t\t\taddLogEntry(\"CODING TO DO: EXCEPTION HANDLING NEEDED: thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance)\");\n\t\t\t}\n\t\t\t\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\t// Dynamic output for a non-verbose run so that the user knows something is happening\n\t\t\t\tif (!appConfig.suppressLoggingOutput) {\n\t\t\t\t\taddProcessingDotEntry();\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Was a paging token error detected? \n\t\t\tif ((thisLevelChildren.type() == JSONType.string) && (thisLevelChildren.str == \"INVALID_PAGING_TOKEN\")) {\n\t\t\t\t// Invalid paging token: failed to parse integer value from token\n\t\t\t\tif (debugLogging) addLogEntry(\"Upstream detected invalid paging token – clearing nextLink and retrying\", [\"debug\"]);\n\t\t\t\tnextLink = null;\n\t\t\t\tthisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance);\n\t\t\t}\n\t\t\t\n\t\t\t// Was a valid JSON response for 'thisLevelChildren' provided?\n\t\t\tif (thisLevelChildren.type() == JSONType.object) {\n\t\t\t\t// process this level children\n\t\t\t\tif (!childParentPath.empty) {\n\t\t\t\t\t// We dont use childParentPath to log, as this poses an information leak risk.\n\t\t\t\t\t// The full parent path of the child, as per the JSON might be:\n\t\t\t\t\t//   /Level 1/Level 2/Level 3/Child Shared Folder/some folder/another folder\n\t\t\t\t\t// But 'Child Shared Folder' is what is shared, thus '/Level 1/Level 2/Level 3/' is a potential information leak if logged.\n\t\t\t\t\t// Plus, the application output now shows accurately what is being shared - so that is a good thing.\n\t\t\t\t\tif (verboseLogging) {addLogEntry(\"Adding \" ~ to!string(count(thisLevelChildren[\"value\"].array)) ~ \" OneDrive JSON items for further processing from \" ~ pathForLogging, [\"verbose\"]);}\n\t\t\t\t}\n\t\t\t\tforeach (child; thisLevelChildren[\"value\"].array) {\n\t\t\t\t\t// 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.\n\t\t\t\t\t// This avoids needless calls to the OneDrive API, and potentially speeds up this process.\n\t\t\t\t\tif (!checkJSONAgainstClientSideFiltering(child)) {\n\t\t\t\t\t\t// add this child to the array of objects\n\t\t\t\t\t\tthisLevelChildrenData ~= child;\n\t\t\t\t\t\t// is this child a folder?\n\t\t\t\t\t\tif (isItemFolder(child)){\n\t\t\t\t\t\t\t// We have to query this folders children if childCount > 0\n\t\t\t\t\t\t\tif (child[\"folder\"][\"childCount\"].integer > 0){\n\t\t\t\t\t\t\t\t// This child folder has children\n\t\t\t\t\t\t\t\tstring childIdToQuery = child[\"id\"].str;\n\t\t\t\t\t\t\t\tstring childDriveToQuery = child[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\t\t\t\tauto grandchildParentPath = child[\"parentReference\"][\"path\"].str.split(\":\");\n\t\t\t\t\t\t\t\tstring folderPathToScan = grandchildParentPath[1] ~ \"/\" ~ child[\"name\"].str;\n\t\t\t\t\t\t\t\tstring newLoggingPath = pathForLogging ~ \"/\" ~ child[\"name\"].str;\n\t\t\t\t\t\t\t\tJSONValue[] grandChildrenData = queryForChildren(childDriveToQuery, childIdToQuery, folderPathToScan, newLoggingPath);\n\t\t\t\t\t\t\t\tforeach (grandChild; grandChildrenData.array) {\n\t\t\t\t\t\t\t\t\t// add the grandchild to the array\n\t\t\t\t\t\t\t\t\tthisLevelChildrenData ~= grandChild;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response \n\t\t\t\t// to indicate more items are available and provide the request URL for the next page of items.\n\t\t\t\tif (\"@odata.nextLink\" in thisLevelChildren) {\n\t\t\t\t\t// Update nextLink to next changeSet bundle\n\t\t\t\t\tnextLink = thisLevelChildren[\"@odata.nextLink\"].str;\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting nextLink to (@odata.nextLink): \" ~ nextLink, [\"debug\"]);}\n\t\t\t\t} else break;\n\t\t\t\n\t\t\t} else {\n\t\t\t\t// Invalid JSON response when querying this level children\n\t\t\t\tif (debugLogging) {addLogEntry(\"INVALID JSON response when attempting a retry of parent function - queryForChildren(driveId, idToQuery, childParentPath, pathForLogging)\", [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// retry thisLevelChildren = queryThisLevelChildren\n\t\t\t\tif (debugLogging) {addLogEntry(\"Thread sleeping for an additional 30 seconds\", [\"debug\"]);}\n\t\t\t\tThread.sleep(dur!\"seconds\"(30));\n\t\t\t\tif (debugLogging) {addLogEntry(\"Retry this call thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance)\", [\"debug\"]);}\n\t\t\t\tthisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance);\n\t\t\t}\n\t\t\t\n\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tqueryChildrenOneDriveApiInstance.releaseCurlEngine();\n\t\tqueryChildrenOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return response\n\t\treturn thisLevelChildrenData;\n\t}\n\t\n\t// Query the OneDrive API for the child objects for this element\n\tJSONValue queryThisLevelChildren(string driveId, string idToQuery, string nextLink, OneDriveApi queryChildrenOneDriveApiInstance) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Issue #3115 - Validate driveId length\n\t\t// - The function 'queryForChildren' checks the 'driveId' value and that value is the input to this function.\n\t\t//   It is redundant to then check 'driveid' again as this is not changed when this function is called\n\t\t\n\t\t// function variables \n\t\tJSONValue thisLevelChildren;\n\t\t\n\t\t// query children\n\t\ttry {\n\t\t\t// attempt API call\n\t\t\tif (debugLogging) {addLogEntry(\"Attempting Query: thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)\", [\"debug\"]);}\n\t\t\tthisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink);\n\t\t\tif (debugLogging) {addLogEntry(\"Query 'thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)' performed successfully\", [\"debug\"]);}\n\t\t} catch (OneDriveException exception) {\n\t\t\t// OneDrive threw an error\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(debugLogBreakType1, [\"debug\"]);\n\t\t\t\taddLogEntry(\"Query Error: thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)\", [\"debug\"]);\n\t\t\t\taddLogEntry(\"driveId: \" ~ driveId, [\"debug\"]);\n\t\t\t\taddLogEntry(\"idToQuery: \" ~ idToQuery, [\"debug\"]);\n\t\t\t\taddLogEntry(\"nextLink: \" ~ nextLink, [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t// Display what the error is\n\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\n\t\t\t// With the error displayed, testing of PR #3381 for #3375 generated this error:\n\t\t\t//\tError Message:       HTTP request returned status code 400 (Bad Request)\n\t\t\t//\tError Reason:        Invalid paging token: failed to parse integer value from token.\n\t\t\tif ((exception.httpStatusCode == 400) && (exception.msg.canFind(\"Invalid paging token\")))  {\n\t\t\t\t// Log and return a known marker that bypasses JSONType.object check\n\t\t\t\tif (debugLogging) addLogEntry(\"Detected invalid paging token – signaling upstream\", [\"debug\"]);\n\t\t\t\treturn JSONValue(\"INVALID_PAGING_TOKEN\");\n\t\t\t}\n\t\t\t\n\t\t\t// Generic failure\n\t\t\treturn thisLevelChildren;\n\t\t}\n\t\t\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return response\n\t\treturn thisLevelChildren;\n\t}\n\t\n\t// Traverses the provided path online, via the OneDrive API, following correct parent driveId and itemId elements across the account\n\t// 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.\n\t//\n\t// If the createPathIfMissing = false + no path exists online, a null invalid JSON item will be returned.\n\t// If the createPathIfMissing = true + no path exists online, the requested path will be created in the correct location online. The resulting\n\t// response to the directory creation will then be returned.\n\t//\n\t// This function also ensures that each path in the requested path actually matches the requested element to ensure that the OneDrive API response\n\t// is not falsely matching a 'case insensitive' match to the actual request which is a POSIX compliance issue.\n\tJSONValue queryOneDriveForSpecificPathAndCreateIfMissing(string thisNewPathToSearch, bool createPathIfMissing) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// function variables\n\t\tJSONValue getPathDetailsAPIResponse;\n\t\tstring currentPathTree;\n\t\tItem parentDetails;\n\t\tJSONValue topLevelChildren;\n\t\tstring nextLink;\n\t\tbool directoryFoundOnline = false;\n\t\tbool posixIssue = false;\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi queryOneDriveForSpecificPath;\n\t\tqueryOneDriveForSpecificPath = new OneDriveApi(appConfig);\n\t\tqueryOneDriveForSpecificPath.initialise();\n\t\t\n\t\tforeach (thisFolderName; pathSplitter(thisNewPathToSearch)) {\n\t\t\tif (debugLogging) {addLogEntry(\"Testing for the existence online of this folder path: \" ~ thisFolderName, [\"debug\"]);}\n\t\t\tdirectoryFoundOnline = false;\n\t\t\t\n\t\t\t// If this is '.' this is the account root\n\t\t\tif (thisFolderName == \".\") {\n\t\t\t\tcurrentPathTree = thisFolderName;\n\t\t\t} else {\n\t\t\t\tcurrentPathTree = currentPathTree ~ \"/\" ~ thisFolderName;\n\t\t\t}\n\t\t\t\n\t\t\t// What path are we querying\n\t\t\tif (debugLogging) {addLogEntry(\"Attempting to query OneDrive for this path: \" ~ currentPathTree, [\"debug\"]);}\n\t\t\t\n\t\t\t// What query do we use?\n\t\t\tif (thisFolderName == \".\") {\n\t\t\t\t// Query the root, set the right details\n\t\t\t\ttry {\n\t\t\t\t\tgetPathDetailsAPIResponse = queryOneDriveForSpecificPath.getPathDetails(currentPathTree);\n\t\t\t\t\tparentDetails = makeItem(getPathDetailsAPIResponse);\n\t\t\t\t\t// Save item to the database\n\t\t\t\t\tsaveItem(getPathDetailsAPIResponse);\n\t\t\t\t\tdirectoryFoundOnline = true;\n\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t// Display what the error is\n\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Ensure we have a valid driveId to search here\n\t\t\t\tif (parentDetails.driveId.empty) {\n\t\t\t\t\tparentDetails.driveId = appConfig.defaultDriveId;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t\t\tparentDetails.driveId = transformToLowerCase(parentDetails.driveId);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If the prior JSON 'getPathDetailsAPIResponse' is on this account driveId .. then continue to use getPathDetails\n\t\t\t\tif (parentDetails.driveId == appConfig.defaultDriveId) {\n\t\t\t\t\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Query OneDrive API for this path\n\t\t\t\t\t\tgetPathDetailsAPIResponse = queryOneDriveForSpecificPath.getPathDetails(currentPathTree);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Portable Operating System Interface (POSIX) testing of JSON response from OneDrive API\n\t\t\t\t\t\tif (hasName(getPathDetailsAPIResponse)) {\n\t\t\t\t\t\t\t// Perform the POSIX evaluation test against the names\n\t\t\t\t\t\t\tif (performPosixTest(thisFolderName, getPathDetailsAPIResponse[\"name\"].str)) {\n\t\t\t\t\t\t\t\tthrow new PosixException(thisFolderName, getPathDetailsAPIResponse[\"name\"].str);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthrow new JsonResponseException(\"Unable to perform POSIX test as the OneDrive API request generated an invalid JSON response\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// No POSIX issue with requested path element\n\t\t\t\t\t\tparentDetails = makeItem(getPathDetailsAPIResponse);\n\t\t\t\t\t\t// Save item to the database\n\t\t\t\t\t\tsaveItem(getPathDetailsAPIResponse);\n\t\t\t\t\t\tdirectoryFoundOnline = true;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Is this JSON a remote object\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Testing if this is a remote Shared Folder\", [\"debug\"]);}\n\t\t\t\t\t\tif (isItemRemote(getPathDetailsAPIResponse)) {\n\t\t\t\t\t\t\t// Remote Directory .. need a DB Tie Record\n\t\t\t\t\t\t\tcreateDatabaseTieRecordForOnlineSharedFolder(parentDetails);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Temp DB Item to bind the 'remote' path to our parent path\n\t\t\t\t\t\t\tItem tempDBItem;\n\t\t\t\t\t\t\t// Set the name\n\t\t\t\t\t\t\ttempDBItem.name = parentDetails.name;\n\t\t\t\t\t\t\t// Set the correct item type\n\t\t\t\t\t\t\ttempDBItem.type = ItemType.dir;\n\t\t\t\t\t\t\t// Set the right elements using the 'remote' of the parent as the 'actual' for this DB Tie\n\t\t\t\t\t\t\ttempDBItem.driveId = parentDetails.remoteDriveId;\n\t\t\t\t\t\t\ttempDBItem.id = parentDetails.remoteId;\n\t\t\t\t\t\t\t// Set the correct mtime\n\t\t\t\t\t\t\ttempDBItem.mtime = parentDetails.mtime;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Update parentDetails to use this temp record\n\t\t\t\t\t\t\tparentDetails = tempDBItem;\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t\t\t\tdirectoryFoundOnline = false;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (PosixException e) {\n\t\t\t\t\t\t// Display POSIX error message\n\t\t\t\t\t\tdisplayPosixErrorMessage(e.msg);\n\t\t\t\t\t\taddLogEntry(\"ERROR: Requested directory to search for and potentially create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online.\");\n\t\t\t\t\t\taddLogEntry(\"ERROR: To resolve, rename this local directory: \" ~ currentPathTree);\n\t\t\t\t\t} catch (JsonResponseException e) {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(e.msg, [\"debug\"]);}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// parentDetails.driveId is not the account drive id - thus will be a remote shared item\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"This parent directory is a remote object this next path will be on a remote drive\", [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// For this parentDetails.driveId, parentDetails.id object, query the OneDrive API for it's children\n\t\t\t\t\twhile (true) {\n\t\t\t\t\t\t// Check if exitHandlerTriggered is true\n\t\t\t\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Query this remote object for its children\n\t\t\t\t\t\ttopLevelChildren = queryOneDriveForSpecificPath.listChildren(parentDetails.driveId, parentDetails.id, nextLink);\n\t\t\t\t\t\t// Process each child\n\t\t\t\t\t\tforeach (child; topLevelChildren[\"value\"].array) {\n\t\t\t\t\t\t\t// Is this child a folder?\n\t\t\t\t\t\t\tif (isItemFolder(child)) {\n\t\t\t\t\t\t\t\t// Is this the child folder we are looking for, and is a POSIX match?\n\t\t\t\t\t\t\t\tif (child[\"name\"].str == thisFolderName) {\n\t\t\t\t\t\t\t\t\t// EXACT MATCH including case sensitivity: Flag that we found the folder online \n\t\t\t\t\t\t\t\t\tdirectoryFoundOnline = true;\n\t\t\t\t\t\t\t\t\t// Use these details for the next entry path\n\t\t\t\t\t\t\t\t\tgetPathDetailsAPIResponse = child;\n\t\t\t\t\t\t\t\t\tparentDetails = makeItem(getPathDetailsAPIResponse);\n\t\t\t\t\t\t\t\t\t// Save item to the database\n\t\t\t\t\t\t\t\t\tsaveItem(getPathDetailsAPIResponse);\n\t\t\t\t\t\t\t\t\t// No need to continue searching\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tstring childAsLower = toLower(child[\"name\"].str);\n\t\t\t\t\t\t\t\t\tstring thisFolderNameAsLower = toLower(thisFolderName);\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\tif (childAsLower == thisFolderNameAsLower) {\t\n\t\t\t\t\t\t\t\t\t\t\t// This is a POSIX 'case in-sensitive match' ..... \n\t\t\t\t\t\t\t\t\t\t\t// Local item name has a 'case-insensitive match' to an existing item on OneDrive\n\t\t\t\t\t\t\t\t\t\t\tposixIssue = true;\n\t\t\t\t\t\t\t\t\t\t\tthrow new PosixException(thisFolderName, child[\"name\"].str);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} catch (PosixException e) {\n\t\t\t\t\t\t\t\t\t\t// Display POSIX error message\n\t\t\t\t\t\t\t\t\t\tdisplayPosixErrorMessage(e.msg);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Requested directory to search for and potentially create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online.\");\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: To resolve, rename this local directory: \" ~ currentPathTree);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\tif (directoryFoundOnline) {\n\t\t\t\t\t\t\t// We found the folder, no need to continue searching nextLink data\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response \n\t\t\t\t\t\t// to indicate more items are available and provide the request URL for the next page of items.\n\t\t\t\t\t\tif (\"@odata.nextLink\" in topLevelChildren) {\n\t\t\t\t\t\t\t// Update nextLink to next changeSet bundle\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting nextLink to (@odata.nextLink): \" ~ nextLink, [\"debug\"]);}\n\t\t\t\t\t\t\tnextLink = topLevelChildren[\"@odata.nextLink\"].str;\n\t\t\t\t\t\t} else break;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\t\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// If we did not find the folder, we need to create this folder\n\t\t\tif (!directoryFoundOnline) {\n\t\t\t\t// Folder not found online\n\t\t\t\t// Set any response to be an invalid JSON item\n\t\t\t\tgetPathDetailsAPIResponse = null;\n\t\t\t\t// Was there a POSIX issue?\n\t\t\t\tif (!posixIssue) {\n\t\t\t\t\t// No POSIX issue\n\t\t\t\t\tif (createPathIfMissing) {\n\t\t\t\t\t\t// Create this path as it is missing on OneDrive online and there is no POSIX issue with a 'case-insensitive match'\n\t\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"FOLDER NOT FOUND ONLINE AND WE ARE REQUESTED TO CREATE IT\", [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"Create folder on this drive:             \" ~ parentDetails.driveId, [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"Create folder as a child on this object: \" ~ parentDetails.id, [\"debug\"]);\n\t\t\t\t\t\t\taddLogEntry(\"Create this folder name:                 \" ~ thisFolderName, [\"debug\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Generate the JSON needed to create the folder online\n\t\t\t\t\t\tJSONValue newDriveItem = [\n\t\t\t\t\t\t\t\t\"name\": JSONValue(thisFolderName),\n\t\t\t\t\t\t\t\t\"folder\": parseJSON(\"{}\")\n\t\t\t\t\t\t];\n\t\t\t\t\t\n\t\t\t\t\t\tJSONValue createByIdAPIResponse;\n\t\t\t\t\t\t// Submit the creation request\n\t\t\t\t\t\t// Fix for https://github.com/skilion/onedrive/issues/356\n\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t// Attempt to create a new folder on the configured parent driveId & parent id\n\t\t\t\t\t\t\t\tcreateByIdAPIResponse = queryOneDriveForSpecificPath.createById(parentDetails.driveId, parentDetails.id, newDriveItem);\n\t\t\t\t\t\t\t\t// Is the response a valid JSON object - validation checking done in saveItem\n\t\t\t\t\t\t\t\tsaveItem(createByIdAPIResponse);\n\t\t\t\t\t\t\t\t// Set getPathDetailsAPIResponse to createByIdAPIResponse\n\t\t\t\t\t\t\t\tgetPathDetailsAPIResponse = createByIdAPIResponse;\n\t\t\t\t\t\t\t} catch (OneDriveException e) {\n\t\t\t\t\t\t\t\t// 409 - API Race Condition\n\t\t\t\t\t\t\t\tif (e.httpStatusCode == 409) {\n\t\t\t\t\t\t\t\t\t// When we attempted to create it, OneDrive responded that it now already exists\n\t\t\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"OneDrive reported that \" ~ thisFolderName ~ \" already exists .. OneDrive API race condition\", [\"verbose\"]);}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// some other error from OneDrive was returned - display what it is\n\t\t\t\t\t\t\t\t\taddLogEntry(\"OneDrive generated an error when creating this path: \" ~ thisFolderName);\n\t\t\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(e.msg, thisFunctionName);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Simulate a successful 'directory create' & save it to the dryRun database copy\n\t\t\t\t\t\t\t// The simulated response has to pass 'makeItem' as part of saveItem\n\t\t\t\t\t\t\tauto fakeResponse = createFakeResponse(thisNewPathToSearch);\n\t\t\t\t\t\t\t// Save item to the database\n\t\t\t\t\t\t\tsaveItem(fakeResponse);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tqueryOneDriveForSpecificPath.releaseCurlEngine();\n\t\tqueryOneDriveForSpecificPath = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Output our search results\n\t\tif (debugLogging) {addLogEntry(\"queryOneDriveForSpecificPathAndCreateIfMissing.getPathDetailsAPIResponse = \" ~ to!string(getPathDetailsAPIResponse), [\"debug\"]);}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return JSON result\n\t\treturn getPathDetailsAPIResponse;\n\t}\n\t\n\t// Delete an item by it's path\n\t// This function is only used in --monitor mode to remove a directory online\n\tvoid deleteByPath(string path) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// function variables\n\t\tItem dbItem;\n\t\t\n\t\t// Need to check all driveid's we know about, not just the defaultDriveId\n\t\tbool itemInDB = false;\n\t\tforeach (searchDriveId; onlineDriveDetails.keys) {\n\t\t\tif (itemDB.selectByPath(path, searchDriveId, dbItem)) {\n\t\t\t\t// item was found in the DB\n\t\t\t\titemInDB = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Was the item found in the database?\n\t\tif (!itemInDB) {\n\t\t\t// path to delete is not in the local database ..\n\t\t\t// was this a --remove-directory attempt?\n\t\t\tif (!appConfig.getValueBool(\"monitor\")) {\n\t\t\t\t// --remove-directory deletion attempt\n\t\t\t\taddLogEntry(\"The item to delete is not in the local database - unable to delete online\");\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\t// normal use .. --monitor being used\n\t\t\t\tthrow new SyncException(\"The item to delete is not in the local database\");\n\t\t\t}\n\t\t}\n\t\t\n\t\t// This needs to be enforced as we have to know the parent id of the object being deleted\n\t\tif (dbItem.parentId == null) {\n\t\t\t// the item is a remote folder, need to do the operation on the parent\n\t\t\tenforce(itemDB.selectByPathIncludingRemoteItems(path, appConfig.defaultDriveId, dbItem));\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tif (noRemoteDelete) {\n\t\t\t\t// do not process remote delete\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Skipping remote delete as --upload-only & --no-remote-delete configured\", [\"verbose\"]);}\n\t\t\t} else {\n\t\t\t\tuploadDeletedItem(dbItem, path);\n\t\t\t}\n\t\t} catch (FileException e) {\n\t\t\t// filesystem generated an error message - display error message\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, path);\n\t\t} catch (OneDriveException e) {\n\t\t\tif (e.httpStatusCode == 404) {\n\t\t\t\taddLogEntry(e.msg);\n\t\t\t} else {\n\t\t\t\t// display what the error is\n\t\t\t\tdisplayOneDriveErrorMessage(e.msg, thisFunctionName);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Delete an item by it's path\n\t// Delete a directory on OneDrive without syncing. This function is only used with --remove-directory\n\tvoid deleteByPathNoSync(string path) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Attempt to delete the requested path within OneDrive without performing a sync\n\t\taddLogEntry(\"Attempting to delete the requested path within Microsoft OneDrive\");\n\t\t\n\t\t// function variables\n\t\tJSONValue getPathDetailsAPIResponse;\n\t\tOneDriveApi deleteByPathNoSyncAPIInstance;\n\t\t\n\t\t// test if the path we are going to exists on OneDrive\n\t\ttry {\n\t\t\t// Create a new API Instance for this thread and initialise it\n\t\t\tdeleteByPathNoSyncAPIInstance = new OneDriveApi(appConfig);\n\t\t\tdeleteByPathNoSyncAPIInstance.initialise();\n\t\t\tgetPathDetailsAPIResponse = deleteByPathNoSyncAPIInstance.getPathDetails(path);\n\t\t\t\n\t\t\t// If we get here, no error, the path to delete exists online\n\n\t\t} catch (OneDriveException exception) {\n\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tdeleteByPathNoSyncAPIInstance.releaseCurlEngine();\n\t\t\tdeleteByPathNoSyncAPIInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\n\t\t\t// Log that an error was generated\n\t\t\tif (debugLogging) {addLogEntry(\"deleteByPathNoSyncAPIInstance.getPathDetails(path) generated a OneDriveException\", [\"debug\"]);}\n\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t// The directory was not found on OneDrive - no need to delete it\n\t\t\t\taddLogEntry(\"The requested directory to delete was not found on OneDrive - skipping removing the remote directory online as it does not exist\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t// Display what the error is\n\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\treturn;\t\n\t\t}\n\t\t\n\t\t// Make a DB item from the JSON data that was returned via the API call\n\t\tItem deletionItem = makeItem(getPathDetailsAPIResponse);\n\t\t\n\t\t// Is the item to remove the correct type\n\t\tif (deletionItem.type == ItemType.dir) {\n\t\t\t// Item is a directory to remove\n\t\t\t// Log that the path | item was found, is a directory\n\t\t\taddLogEntry(\"The requested directory to delete was found on OneDrive - attempting deletion\");\n\t\t\t\n\t\t\t// Try the online deletion\n\t\t\ttry {\n\t\t\t\tif (!permanentDelete) {\n\t\t\t\t\t// Perform the delete via the default OneDrive API instance\n\t\t\t\t\tdeleteByPathNoSyncAPIInstance.deleteById(deletionItem.driveId, deletionItem.id);\n\t\t\t\t} else {\n\t\t\t\t\t// Perform the permanent delete via the default OneDrive API instance\n\t\t\t\t\tdeleteByPathNoSyncAPIInstance.permanentDeleteById(deletionItem.driveId, deletionItem.id);\n\t\t\t\t}\n\t\t\t\t// If we get here without error, directory was deleted\n\t\t\t\taddLogEntry(\"The requested directory to delete online has been deleted\");\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// Display what the error is\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t} else {\n\t\t\t// --remove-directory is for removing directories\n\t\t\t// Log that the path | item was found, is a directory\n\t\t\taddLogEntry(\"The requested path to delete is not a directory - aborting deletion attempt\");\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tdeleteByPathNoSyncAPIInstance.releaseCurlEngine();\n\t\tdeleteByPathNoSyncAPIInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_move\n\t// This function is only called in monitor mode when an move event is coming from\n\t// inotify and we try to move the item.\n\tvoid uploadMoveItem(string oldPath, string newPath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Log that we are doing a move\n\t\taddLogEntry(\"Moving \" ~ oldPath ~ \" to \" ~ newPath);\n\t\t// Is this move unwanted?\n\t\tbool unwanted = false;\n\t\t// Item variables\n\t\tItem oldItem, newItem, parentItem;\n\t\t\n\t\t// This not a Client Side Filtering check, nor a Microsoft Check, but is a sanity check that the path provided is UTF encoded correctly\n\t\t// Check the std.encoding of the path against: Unicode 5.0, ASCII, ISO-8859-1, ISO-8859-2, WINDOWS-1250, WINDOWS-1251, WINDOWS-1252\n\t\tif (!unwanted) {\n\t\t\tif(!isValid(newPath)) {\n\t\t\t\t// Path is not valid according to https://dlang.org/phobos/std_encoding.html\n\t\t\t\taddLogEntry(\"Skipping item - invalid character encoding sequence: \" ~ newPath, [\"info\", \"notify\"]);\n\t\t\t\tunwanted = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Check this path against the Client Side Filtering Rules\n\t\t// - check_nosync\n\t\t// - skip_dotfiles\n\t\t// - skip_symlinks\n\t\t// - skip_file\n\t\t// - skip_dir\n\t\t// - sync_list\n\t\t// - skip_size\n\t\tif (!unwanted) {\n\t\t\tunwanted = checkPathAgainstClientSideFiltering(newPath);\n\t\t}\n\t\t\n\t\t// Check this path against the Microsoft Naming Conventions & Restrictions\n\t\t// - Check path against Microsoft OneDrive restriction and limitations about Windows naming for files and folders\n\t\t// - Check path for bad whitespace items\n\t\t// - Check path for HTML ASCII Codes\n\t\t// - Check path for ASCII Control Codes\n\t\tif (!unwanted) {\n\t\t\tunwanted = checkPathAgainstMicrosoftNamingRestrictions(newPath);\n\t\t}\n\t\t\n\t\t// 'newPath' has passed client side filtering validation\n\t\tif (!unwanted) {\n\t\t\n\t\t\tif (!itemDB.selectByPath(oldPath, appConfig.defaultDriveId, oldItem)) {\n\t\t\t\t// The old path|item is not synced with the database, upload as a new file\n\t\t\t\taddLogEntry(\"Moved local item was not in-sync with local database - uploading as new item\");\n\t\t\t\tscanLocalFilesystemPathForNewData(newPath);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\n\t\t\tif (oldItem.parentId == null) {\n\t\t\t\t// the item is a remote folder, need to do the operation on the parent\n\t\t\t\tenforce(itemDB.selectByPathIncludingRemoteItems(oldPath, appConfig.defaultDriveId, oldItem));\n\t\t\t}\n\t\t\n\t\t\tif (itemDB.selectByPath(newPath, appConfig.defaultDriveId, newItem)) {\n\t\t\t\t// the destination has been overwritten\n\t\t\t\taddLogEntry(\"Moved local item overwrote an existing item - deleting old online item\");\n\t\t\t\tuploadDeletedItem(newItem, newPath);\n\t\t\t}\n\t\t\t\n\t\t\tif (!itemDB.selectByPath(dirName(newPath), appConfig.defaultDriveId, parentItem)) {\n\t\t\t\t// the parent item is not in the database\n\t\t\t\tthrow new SyncException(\"Can't move an item to an unsynchronised directory\");\n\t\t\t}\n\t\t\n\t\t\tif (oldItem.driveId != parentItem.driveId) {\n\t\t\t\t// items cannot be moved between drives\n\t\t\t\tuploadDeletedItem(oldItem, oldPath);\n\t\t\t\t\n\t\t\t\t// what sort of move is this?\n\t\t\t\tif (isFile(newPath)) {\n\t\t\t\t\t// newPath is a file\n\t\t\t\t\tuploadNewFile(newPath);\n\t\t\t\t} else {\n\t\t\t\t\t// newPath is a directory\n\t\t\t\t\tscanLocalFilesystemPathForNewData(newPath);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (!exists(newPath)) {\n\t\t\t\t\t// is this --monitor use?\n\t\t\t\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"uploadMoveItem target has disappeared: \" ~ newPath, [\"verbose\"]);}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\n\t\t\t\t// Configure the modification JSON item\n\t\t\t\tSysTime mtime;\n\t\t\t\tif (appConfig.getValueBool(\"monitor\")) {\n\t\t\t\t\t// Use the newPath modified timestamp\n\t\t\t\t\tmtime = timeLastModified(newPath).toUTC();\n\t\t\t\t} else {\n\t\t\t\t\t// Use the current system time\n\t\t\t\t\tmtime = Clock.currTime().toUTC();\n\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\tJSONValue data = [\n\t\t\t\t\t\"name\": JSONValue(baseName(newPath)),\n\t\t\t\t\t\"parentReference\": JSONValue([\n\t\t\t\t\t\t\"id\": parentItem.id\n\t\t\t\t\t]),\n\t\t\t\t\t\"fileSystemInfo\": JSONValue([\n\t\t\t\t\t\t\"lastModifiedDateTime\": mtime.toISOExtString()\n\t\t\t\t\t])\n\t\t\t\t];\n\t\t\t\t\n\t\t\t\t// Perform the move operation on OneDrive\n\t\t\t\tbool isMoveSuccess = false;\n\t\t\t\tJSONValue response;\n\t\t\t\tstring eTag = oldItem.eTag;\n\t\t\t\t\n\t\t\t\t// Create a new API Instance for this thread and initialise it\n\t\t\t\tOneDriveApi movePathOnlineApiInstance;\n\t\t\t\tmovePathOnlineApiInstance = new OneDriveApi(appConfig);\n\t\t\t\tmovePathOnlineApiInstance.initialise();\n\t\t\t\t\n\t\t\t\t// Try the online move\n\t\t\t\tfor (int i = 0; i < 3; i++) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tresponse = movePathOnlineApiInstance.updateById(oldItem.driveId, oldItem.id, data, eTag);\n\t\t\t\t\t\tisMoveSuccess = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} catch (OneDriveException e) {\n\t\t\t\t\t\t// Handle a 412 - A precondition provided in the request (such as an if-match header) does not match the resource's current state.\n\t\t\t\t\t\tif (e.httpStatusCode == 412) {\n\t\t\t\t\t\t\t// OneDrive threw a 412 error, most likely: ETag does not match current item's value\n\t\t\t\t\t\t\t// Retry without eTag\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"File Move Failed - OneDrive eTag / cTag match issue\", [\"debug\"]);}\n\t\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"OneDrive returned a 'HTTP 412 - Precondition Failed' when attempting to move the file - gracefully handling error\", [\"verbose\"]);}\n\t\t\t\t\t\t\teTag = null;\n\t\t\t\t\t\t\t// Retry to move the file but without the eTag, via the for() loop\n\t\t\t\t\t\t} else if (e.httpStatusCode == 409) {\n\t\t\t\t\t\t\t// Destination item already exists and is a conflict, delete existing item first\n\t\t\t\t\t\t\taddLogEntry(\"Moved local item will overwrite an existing online item - deleting old online item first\");\n\t\t\t\t\t\t\tuploadDeletedItem(newItem, newPath);\n\t\t\t\t\t\t} else\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} \n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tmovePathOnlineApiInstance.releaseCurlEngine();\n\t\t\t\tmovePathOnlineApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Save the move response from OneDrive in the database\n\t\t\t\tif (isMoveSuccess && response.type() == JSONType.object) {\n\t\t\t\t\tsaveItem(response);\n\t\t\t\t} else {\n\t\t\t\t\t// Log why we are not saving\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"uploadMoveItem: skipping saveItem() (no JSON payload returned or move not successful)\", [\"debug\"]);}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Moved item is unwanted\n\t\t\taddLogEntry(\"Item has been moved to a location that is excluded from sync operations. Removing item from OneDrive\");\n\t\t\tuploadDeletedItem(oldItem, oldPath);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Perform integrity validation of the file that was uploaded\n\tbool performUploadIntegrityValidationChecks(JSONValue uploadResponse, string localFilePath, long localFileSize) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tbool integrityValid = false;\n\t\n\t\tif (!disableUploadValidation) {\n\t\t\t// Integrity validation has not been disabled (this is the default so we are always integrity checking our uploads)\n\t\t\tif (uploadResponse.type() == JSONType.object) {\n\t\t\t\t// Provided JSON is a valid JSON\n\t\t\t\tlong uploadFileSize;\n\t\t\t\tstring uploadFileHash;\n\t\t\t\tstring localFileHash;\n\t\t\t\t// Regardless if valid JSON is responded with, 'size' and 'quickXorHash' must be present\n\t\t\t\tif (hasFileSize(uploadResponse) && hasQuickXorHash(uploadResponse)) {\n\t\t\t\t\tuploadFileSize = uploadResponse[\"size\"].integer;\n\t\t\t\t\tuploadFileHash = uploadResponse[\"file\"][\"hashes\"][\"quickXorHash\"].str;\n\t\t\t\t\tlocalFileHash = computeQuickXorHash(localFilePath);\n\t\t\t\t} else {\n\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\taddLogEntry(\"Online file validation unable to be performed: input JSON whilst valid did not contain data which could be validated\", [\"verbose\"]);\n\t\t\t\t\t\taddLogEntry(\"WARNING: Skipping upload integrity check for: \" ~ localFilePath, [\"verbose\"]);\n\t\t\t\t\t}\n\t\t\t\t\treturn integrityValid;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// compare values\n\t\t\t\tif ((localFileSize == uploadFileSize) && (localFileHash == uploadFileHash)) {\n\t\t\t\t\t// Uploaded file integrity intact\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Uploaded local file matches reported online size and hash values\", [\"debug\"]);}\n\t\t\t\t\t// set to true and return\n\t\t\t\t\tintegrityValid = true;\n\t\t\t\t\treturn integrityValid;\n\t\t\t\t} else {\n\t\t\t\t\t// Upload integrity failure .. what failed?\n\t\t\t\t\t// There are 2 scenarios where this happens:\n\t\t\t\t\t// 1. Failed Transfer\n\t\t\t\t\t// 2. Upload file is going to a SharePoint Site, where Microsoft enriches the file with additional metadata with no way to disable\n\t\t\t\t\taddLogEntry(\"WARNING: Online file integrity failure for: \" ~ localFilePath, [\"info\", \"notify\"]);\n\t\t\t\t\t\n\t\t\t\t\t// What integrity failed - size?\n\t\t\t\t\tif (localFileSize != uploadFileSize) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"WARNING: Online file integrity failure - Size Mismatch\", [\"verbose\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// What integrity failed - hash?\n\t\t\t\t\tif (localFileHash != uploadFileHash) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"WARNING: Online file integrity failure - Hash Mismatch\", [\"verbose\"]);}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// What account type is this?\n\t\t\t\t\tif (appConfig.accountType != \"personal\") {\n\t\t\t\t\t\t// Not a personal account, thus the integrity failure is most likely due to SharePoint\n\t\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\t\taddLogEntry(\"CAUTION: When you upload files to Microsoft OneDrive that uses SharePoint as its backend, Microsoft OneDrive will alter your files post upload.\", [\"verbose\"]);\n\t\t\t\t\t\t\taddLogEntry(\"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\"]);\n\t\t\t\t\t\t\taddLogEntry(\"CAUTION: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details.\", [\"verbose\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// How can this be disabled?\n\t\t\t\t\taddLogEntry(\"To disable the integrity checking of uploaded files use --disable-upload-validation\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (verboseLogging) {\n\t\t\t\t\taddLogEntry(\"Online file validation unable to be performed: input JSON whilst valid did not contain data which could be validated\", [\"verbose\"]);\n\t\t\t\t\taddLogEntry(\"WARNING: Skipping upload integrity check for: \" ~ localFilePath, [\"verbose\"]);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Skipping upload integrity check, do not notify the user via the GUI ... they have explicitly disabled upload validation\n\t\t\tif (verboseLogging) {addLogEntry(\"WARNING: Skipping upload integrity check for: \" ~ localFilePath, [\"verbose\"]);}\n\t\t\t\n\t\t\t// We are bypassing integrity checks due to --disable-upload-validation\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"Online file validation disabled due to --disable-upload-validation\", [\"debug\"]);\n\t\t\t\taddLogEntry(\"- Assuming file integrity is OK and valid\", [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Ensure we return 'true', but this is in a false sense, as we are skipping the integrity check, so we assume the file is good\n\t\t\tintegrityValid = true;\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Is the file integrity online valid?\n\t\treturn integrityValid;\n\t}\n\t\n\t// Query Office 365 SharePoint Shared Library site name to obtain it's Drive ID\n\tvoid querySiteCollectionForDriveID(string sharepointLibraryNameToQuery) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Steps to get the ID:\n\t\t// 1. Query https://graph.microsoft.com/v1.0/sites?search= with the name entered\n\t\t// 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\n\t\t// 3. If valid, use the returned ID and query the site drives\n\t\t//\t\thttps://graph.microsoft.com/v1.0/sites/<site_id>/drives\n\t\t// 4. Display Shared Library Name & Drive ID\n\t\t\n\t\tstring site_id;\n\t\tstring drive_id;\n\t\tbool found = false;\n\t\tJSONValue siteQuery;\n\t\tstring nextLink;\n\t\tstring[] siteSearchResults;\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi querySharePointLibraryNameApiInstance;\n\t\tquerySharePointLibraryNameApiInstance = new OneDriveApi(appConfig);\n\t\tquerySharePointLibraryNameApiInstance.initialise();\n\t\t\n\t\t// The account type must not be a personal account type\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\taddLogEntry(\"ERROR: A OneDrive Personal Account cannot be used with --get-sharepoint-drive-id. Please re-authenticate your client using a OneDrive Business Account.\");\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// What query are we performing?\n\t\taddLogEntry();\n\t\taddLogEntry(\"Office 365 Library Name Query: \" ~ sharepointLibraryNameToQuery);\n\t\t\n\t\twhile (true) {\n\t\t\t// Check if exitHandlerTriggered is true\n\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\n\t\t\ttry {\n\t\t\t\tsiteQuery = querySharePointLibraryNameApiInstance.o365SiteSearch(nextLink);\n\t\t\t} catch (OneDriveException e) {\n\t\t\t\taddLogEntry(\"ERROR: Query of OneDrive for Office 365 Library Name failed\");\n\t\t\t\t// Forbidden - most likely authentication scope needs to be updated\n\t\t\t\tif (e.httpStatusCode == 403) {\n\t\t\t\t\taddLogEntry(\"ERROR: Authentication scope needs to be updated. Use --reauth and re-authenticate client.\");\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\tquerySharePointLibraryNameApiInstance.releaseCurlEngine();\n\t\t\t\t\tquerySharePointLibraryNameApiInstance = null;\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Requested resource cannot be found\n\t\t\t\tif (e.httpStatusCode == 404) {\n\t\t\t\t\tstring siteSearchUrl;\n\t\t\t\t\tif (nextLink.empty) {\n\t\t\t\t\t\tsiteSearchUrl = querySharePointLibraryNameApiInstance.getSiteSearchUrl();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsiteSearchUrl = nextLink;\n\t\t\t\t\t}\n\t\t\t\t\t// log the error\n\t\t\t\t\taddLogEntry(\"ERROR: Your OneDrive Account and Authentication Scope cannot access this OneDrive API: \" ~ siteSearchUrl);\n\t\t\t\t\taddLogEntry(\"ERROR: To resolve, please discuss this issue with whomever supports your OneDrive and SharePoint environment.\");\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\tquerySharePointLibraryNameApiInstance.releaseCurlEngine();\n\t\t\t\t\tquerySharePointLibraryNameApiInstance = null;\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t// Display what the error is\n\t\t\t\tdisplayOneDriveErrorMessage(e.msg, thisFunctionName);\n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tquerySharePointLibraryNameApiInstance.releaseCurlEngine();\n\t\t\t\tquerySharePointLibraryNameApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\t// is siteQuery a valid JSON object & contain data we can use?\n\t\t\tif ((siteQuery.type() == JSONType.object) && (\"value\" in siteQuery)) {\n\t\t\t\t// valid JSON object\n\t\t\t\tif (debugLogging) {addLogEntry(\"O365 Query Response: \" ~ to!string(siteQuery), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\tforeach (searchResult; siteQuery[\"value\"].array) {\n\t\t\t\t\t// Need an 'exclusive' match here with sharepointLibraryNameToQuery as entered\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Found O365 Site: \" ~ to!string(searchResult), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// 'displayName' and 'id' have to be present in the search result record in order to query the site\n\t\t\t\t\tif ((\"displayName\" in searchResult) && (\"id\" in searchResult)) {\n\t\t\t\t\t\tif (sharepointLibraryNameToQuery == searchResult[\"displayName\"].str){\n\t\t\t\t\t\t\t// 'displayName' matches search request\n\t\t\t\t\t\t\tsite_id = searchResult[\"id\"].str;\n\t\t\t\t\t\t\tJSONValue siteDriveQuery;\n\t\t\t\t\t\t\tstring nextLinkDrive;\n\n\t\t\t\t\t\t\twhile (true) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tsiteDriveQuery = querySharePointLibraryNameApiInstance.o365SiteDrives(site_id, nextLinkDrive);\n\t\t\t\t\t\t\t\t} catch (OneDriveException e) {\n\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Query of OneDrive for Office Site ID failed\");\n\t\t\t\t\t\t\t\t\t// display what the error is\n\t\t\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(e.msg, thisFunctionName);\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\t\t\t\tquerySharePointLibraryNameApiInstance.releaseCurlEngine();\n\t\t\t\t\t\t\t\t\tquerySharePointLibraryNameApiInstance = null;\n\t\t\t\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t// is siteDriveQuery a valid JSON object & contain data we can use?\n\t\t\t\t\t\t\t\tif ((siteDriveQuery.type() == JSONType.object) && (\"value\" in siteDriveQuery)) {\n\t\t\t\t\t\t\t\t\t// valid JSON object\n\t\t\t\t\t\t\t\t\tforeach (driveResult; siteDriveQuery[\"value\"].array) {\n\t\t\t\t\t\t\t\t\t\t// Display results\n\t\t\t\t\t\t\t\t\t\tfound = true;\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"-----------------------------------------------\");\n\t\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Site Details: \" ~ to!string(driveResult), [\"debug\"]);}\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Site Name:    \" ~ searchResult[\"displayName\"].str);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Library Name: \" ~ driveResult[\"name\"].str);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"drive_id:     \" ~ driveResult[\"id\"].str);\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"Library URL:  \" ~ driveResult[\"webUrl\"].str);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\n\t\t\t\t\t\t\t\t\t// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response \n\t\t\t\t\t\t\t\t\t// to indicate more items are available and provide the request URL for the next page of items.\n\t\t\t\t\t\t\t\t\tif (\"@odata.nextLink\" in siteDriveQuery) {\n\t\t\t\t\t\t\t\t\t\t// Update nextLink to next set of SharePoint library names\n\t\t\t\t\t\t\t\t\t\tnextLinkDrive = siteDriveQuery[\"@odata.nextLink\"].str;\n\t\t\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting nextLinkDrive to (@odata.nextLink): \" ~ nextLinkDrive, [\"debug\"]);}\n\n\t\t\t\t\t\t\t\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\t\t\t\t\t\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// closeout\n\t\t\t\t\t\t\t\t\t\taddLogEntry(\"-----------------------------------------------\");\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// not a valid JSON object\n\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: There was an error performing this operation on Microsoft OneDrive\");\n\t\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Increase logging verbosity to assist determining why.\");\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\t\t\t\tquerySharePointLibraryNameApiInstance.releaseCurlEngine();\n\t\t\t\t\t\t\t\t\tquerySharePointLibraryNameApiInstance = null;\n\t\t\t\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// 'displayName', 'id' or ''webUrl' not present in JSON results for a specific site\n\t\t\t\t\t\tstring siteNameAvailable = \"Site 'name' was restricted by OneDrive API permissions\";\n\t\t\t\t\t\tbool displayNameAvailable = false;\n\t\t\t\t\t\tbool idAvailable = false;\n\t\t\t\t\t\tif (\"name\" in searchResult) siteNameAvailable = searchResult[\"name\"].str;\n\t\t\t\t\t\tif (\"displayName\" in searchResult) displayNameAvailable = true;\n\t\t\t\t\t\tif (\"id\" in searchResult) idAvailable = true;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Display error details for this site data\n\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\taddLogEntry(\"ERROR: SharePoint Site details not provided for: \" ~ siteNameAvailable);\n\t\t\t\t\t\taddLogEntry(\"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.\");\n\t\t\t\t\t\taddLogEntry(\"ERROR: Your site security settings is preventing the following details from being accessed: 'displayName' or 'id'\");\n\t\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\t\taddLogEntry(\" - Is 'displayName' available = \" ~ to!string(displayNameAvailable), [\"verbose\"]);\n\t\t\t\t\t\t\taddLogEntry(\" - Is 'id' available          = \" ~ to!string(idAvailable), [\"verbose\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\taddLogEntry(\"ERROR: To debug this further, please increase application output verbosity to provide further insight as to what details are actually being returned.\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif(!found) {\n\t\t\t\t\t// The SharePoint site we are searching for was not found in this bundle set\n\t\t\t\t\t// Add to siteSearchResults so we can display what we did find\n\t\t\t\t\tstring siteSearchResultsEntry;\n\t\t\t\t\tforeach (searchResult; siteQuery[\"value\"].array) {\n\t\t\t\t\t\t// We can only add the displayName if it is available\n\t\t\t\t\t\tif (\"displayName\" in searchResult) {\n\t\t\t\t\t\t\t// Use the displayName\n\t\t\t\t\t\t\tsiteSearchResultsEntry = \" * \" ~ searchResult[\"displayName\"].str;\n\t\t\t\t\t\t\tsiteSearchResults ~= siteSearchResultsEntry;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Add, but indicate displayName unavailable, use id\n\t\t\t\t\t\t\tif (\"id\" in searchResult) {\n\t\t\t\t\t\t\t\tsiteSearchResultsEntry = \" * \" ~ \"Unknown displayName (Data not provided by API), Site ID: \" ~ searchResult[\"id\"].str;\n\t\t\t\t\t\t\t\tsiteSearchResults ~= siteSearchResultsEntry;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// displayName and id unavailable, display in debug log the entry\n\t\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Bad SharePoint Data for site: \" ~ to!string(searchResult), [\"debug\"]);}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// not a valid JSON object\n\t\t\t\taddLogEntry(\"ERROR: There was an error performing this operation on Microsoft OneDrive\");\n\t\t\t\taddLogEntry(\"ERROR: Increase logging verbosity to assist determining why.\");\n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tquerySharePointLibraryNameApiInstance.releaseCurlEngine();\n\t\t\t\tquerySharePointLibraryNameApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\t// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response \n\t\t\t// to indicate more items are available and provide the request URL for the next page of items.\n\t\t\tif (\"@odata.nextLink\" in siteQuery) {\n\t\t\t\t// Update nextLink to next set of SharePoint library names\n\t\t\t\tnextLink = siteQuery[\"@odata.nextLink\"].str;\n\t\t\t\tif (debugLogging) {addLogEntry(\"Setting nextLink to (@odata.nextLink): \" ~ nextLink, [\"debug\"]);}\n\t\t\t} else break;\n\t\t\t\n\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t}\n\t\t\n\t\t// Was the intended target found?\n\t\tif(!found) {\n\t\t\t// Was the search a wildcard?\n\t\t\tif (sharepointLibraryNameToQuery != \"*\") {\n\t\t\t\t// Only print this out if the search was not a wildcard\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: The requested SharePoint site could not be found. Please check it's name and your permissions to access the site.\");\n\t\t\t}\n\t\t\t// List all sites returned to assist user\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"The following SharePoint site names were returned:\");\n\t\t\tforeach (searchResultEntry; siteSearchResults) {\n\t\t\t\t// list the display name that we use to match against the user query\n\t\t\t\taddLogEntry(searchResultEntry);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tquerySharePointLibraryNameApiInstance.releaseCurlEngine();\n\t\tquerySharePointLibraryNameApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Query the sync status of the client and the local system\n\tvoid queryOneDriveForSyncStatus(string pathToQueryStatusOn) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Query the account driveId and rootId to get the /delta JSON information\n\t\t// Process that JSON data for relevancy\n\t\t\n\t\t// Function variables\n\t\tlong downloadSize = 0;\n\t\tstring deltaLink = null;\n\t\tstring driveIdToQuery = appConfig.defaultDriveId;\n\t\tstring itemIdToQuery = appConfig.defaultRootId;\n\t\tJSONValue deltaChanges;\n\t\t\n\t\t// Array of JSON items\n\t\tJSONValue[] jsonItemsArray;\n\t\t\n\t\t// Query Database for a potential deltaLink starting point\n\t\tdeltaLink = itemDB.getDeltaLink(driveIdToQuery, itemIdToQuery);\n\t\t\n\t\t// Log what we are doing\n\t\taddProcessingLogHeaderEntry(\"Querying the change status of Drive ID: \" ~ driveIdToQuery, appConfig.verbosityCount);\n\t\t\n\t\t// Create a new API Instance for querying the actual /delta and initialise it\n\t\tOneDriveApi getDeltaDataOneDriveApiInstance;\n\t\tgetDeltaDataOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tgetDeltaDataOneDriveApiInstance.initialise();\n\t\t\n\t\twhile (true) {\n\t\t\t// Check if exitHandlerTriggered is true\n\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t\n\t\t\t// Add a processing '.'\n\t\t\tif (appConfig.verbosityCount == 0) {\n\t\t\t\taddProcessingDotEntry();\n\t\t\t}\n\t\t\n\t\t\t// Get the /delta changes via the OneDrive API\n\t\t\t// getDeltaChangesByItemId has the re-try logic for transient errors\n\t\t\tdeltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, deltaLink, getDeltaDataOneDriveApiInstance);\n\t\t\t\n\t\t\t// If the initial deltaChanges response is an invalid JSON object, keep trying until we get a valid response ..\n\t\t\tif (deltaChanges.type() != JSONType.object) {\n\t\t\t\t// While the response is not a JSON Object or the Exit Handler has not been triggered\n\t\t\t\twhile (deltaChanges.type() != JSONType.object) {\n\t\t\t\t\t// Handle the invalid JSON response and retry\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"ERROR: Query of the OneDrive API via deltaChanges = getDeltaChangesByItemId() returned an invalid JSON response\", [\"debug\"]);}\n\t\t\t\t\tdeltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, deltaLink, getDeltaDataOneDriveApiInstance);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// We have a valid deltaChanges JSON array. This means we have at least 200+ JSON items to process.\n\t\t\t// 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\n\t\t\tforeach (onedriveJSONItem; deltaChanges[\"value\"].array) {\n\t\t\t\t// is the JSON a root object - we dont want to count this\n\t\t\t\tif (!isItemRoot(onedriveJSONItem)) {\n\t\t\t\t\t// Files are the only item that we want to calculate\n\t\t\t\t\tif (isItemFile(onedriveJSONItem)) {\n\t\t\t\t\t\t// JSON item is a file\n\t\t\t\t\t\t// Is the item filtered out due to client side filtering rules?\n\t\t\t\t\t\tif (!checkJSONAgainstClientSideFiltering(onedriveJSONItem)) {\n\t\t\t\t\t\t\t// Is the path of this JSON item 'in-scope' or 'out-of-scope' ?\n\t\t\t\t\t\t\tif (pathToQueryStatusOn != \"/\") {\n\t\t\t\t\t\t\t\t// We need to check the path of this item against pathToQueryStatusOn\n\t\t\t\t\t\t\t\tstring thisItemPath = \"\";\n\t\t\t\t\t\t\t\tif ((\"path\" in onedriveJSONItem[\"parentReference\"]) != null) {\n\t\t\t\t\t\t\t\t\t// If there is a parent reference path, try and use it\n\t\t\t\t\t\t\t\t\tstring selfBuiltPath = onedriveJSONItem[\"parentReference\"][\"path\"].str ~ \"/\" ~ onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Check for ':' and split if present\n\t\t\t\t\t\t\t\t\tauto splitIndex = selfBuiltPath.indexOf(\":\");\n\t\t\t\t\t\t\t\t\tif (splitIndex != -1) {\n\t\t\t\t\t\t\t\t\t\t// Keep only the part after ':'\n\t\t\t\t\t\t\t\t\t\tselfBuiltPath = selfBuiltPath[splitIndex + 1 .. $];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t// Set thisItemPath to the self built path\n\t\t\t\t\t\t\t\t\tthisItemPath = selfBuiltPath;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// no parent reference path available\n\t\t\t\t\t\t\t\t\tthisItemPath = onedriveJSONItem[\"name\"].str;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// can we find 'pathToQueryStatusOn' in 'thisItemPath' ?\n\t\t\t\t\t\t\t\tif (canFind(thisItemPath, pathToQueryStatusOn)) {\n\t\t\t\t\t\t\t\t\t// Add this to the array for processing\n\t\t\t\t\t\t\t\t\tjsonItemsArray ~= onedriveJSONItem;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// We are not doing a --single-directory check\n\t\t\t\t\t\t\t\t// Add this to the array for processing\n\t\t\t\t\t\t\t\tjsonItemsArray ~= onedriveJSONItem;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// The response may contain either @odata.deltaLink or @odata.nextLink\n\t\t\tif (\"@odata.deltaLink\" in deltaChanges) {\n\t\t\t\tdeltaLink = deltaChanges[\"@odata.deltaLink\"].str;\n\t\t\t\tif (debugLogging) {addLogEntry(\"Setting next deltaLink to (@odata.deltaLink): \" ~ deltaLink, [\"debug\"]);}\n\t\t\t}\n\t\t\t\n\t\t\t// Update deltaLink to next changeSet bundle\n\t\t\tif (\"@odata.nextLink\" in deltaChanges) {\t\n\t\t\t\tdeltaLink = deltaChanges[\"@odata.nextLink\"].str;\n\t\t\t\tif (debugLogging) {addLogEntry(\"Setting next deltaLink to (@odata.nextLink): \" ~ deltaLink, [\"debug\"]);}\n\t\t\t} else break;\n\t\t\t\n\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t}\n\t\t\n\t\t// Terminate getDeltaDataOneDriveApiInstance here\n\t\tgetDeltaDataOneDriveApiInstance.releaseCurlEngine();\n\t\tgetDeltaDataOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection on this destroyed curl engine\n\t\tGC.collect();\n\t\t\n\t\t// Needed after printing out '....' when fetching changes from OneDrive API\n\t\tif (appConfig.verbosityCount == 0) {\n\t\t\tcompleteProcessingDots();\n\t\t}\n\t\t\n\t\t// Are there any JSON items to process?\n\t\tif (count(jsonItemsArray) != 0) {\n\t\t\t// There are items to process\n\t\t\tforeach (onedriveJSONItem; jsonItemsArray.array) {\n\t\t\t\n\t\t\t\t// variables we need\n\t\t\t\tstring thisItemParentDriveId;\n\t\t\t\tstring thisItemId;\n\t\t\t\tstring thisItemHash;\n\t\t\t\tbool existingDBEntry = false;\n\t\t\t\t\n\t\t\t\t// Is this file a remote item (on a shared folder) ?\n\t\t\t\tif (isItemRemote(onedriveJSONItem)) {\n\t\t\t\t\t// remote drive item\n\t\t\t\t\tthisItemParentDriveId = onedriveJSONItem[\"remoteItem\"][\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tthisItemId = onedriveJSONItem[\"id\"].str;\n\t\t\t\t} else {\n\t\t\t\t\t// standard drive item\n\t\t\t\t\tthisItemParentDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\tthisItemId = onedriveJSONItem[\"id\"].str;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Get the file hash\n\t\t\t\tif (hasHashes(onedriveJSONItem)) {\n\t\t\t\t\t// At a minimum we require 'quickXorHash' to exist\n\t\t\t\t\tif (hasQuickXorHash(onedriveJSONItem)) {\n\t\t\t\t\t\t// JSON item has a hash we can use\n\t\t\t\t\t\tthisItemHash = onedriveJSONItem[\"file\"][\"hashes\"][\"quickXorHash\"].str;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Check if the item has been seen before\n\t\t\t\t\tItem existingDatabaseItem;\n\t\t\t\t\texistingDBEntry = itemDB.selectById(thisItemParentDriveId, thisItemId, existingDatabaseItem);\n\t\t\t\t\t\n\t\t\t\t\tif (existingDBEntry) {\n\t\t\t\t\t\t// item exists in database .. do the database details match the JSON record?\n\t\t\t\t\t\tif (existingDatabaseItem.quickXorHash != thisItemHash) {\n\t\t\t\t\t\t\t// file hash is different, this will trigger a download event\n\t\t\t\t\t\t\tif (hasFileSize(onedriveJSONItem)) {\n\t\t\t\t\t\t\t\tdownloadSize = downloadSize + onedriveJSONItem[\"size\"].integer;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} \n\t\t\t\t\t} else {\n\t\t\t\t\t\t// item does not exist in the database\n\t\t\t\t\t\t// this item has already passed client side filtering rules (skip_dir, skip_file, sync_list)\n\t\t\t\t\t\t// this will trigger a download event\n\t\t\t\t\t\tif (hasFileSize(onedriveJSONItem)) {\n\t\t\t\t\t\t\tdownloadSize = downloadSize + onedriveJSONItem[\"size\"].integer;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\t\n\t\t// Was anything detected that would constitute a download?\n\t\tif (downloadSize > 0) {\n\t\t\t// we have something to download\n\t\t\tif (pathToQueryStatusOn != \"/\") {\n\t\t\t\taddLogEntry(\"The selected local directory via --single-directory is out of sync with Microsoft OneDrive\");\n\t\t\t} else {\n\t\t\t\taddLogEntry(\"The configured local 'sync_dir' directory is out of sync with Microsoft OneDrive\");\n\t\t\t}\n\t\t\taddLogEntry(\"Approximate data to download from Microsoft OneDrive: \" ~ to!string(downloadSize/1024) ~ \" KB\");\n\t\t} else {\n\t\t\t// No changes were returned\n\t\t\taddLogEntry(\"There are no pending changes from Microsoft OneDrive; your local directory matches the data online.\");\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Query OneDrive for file details of a given path, returning either the 'webURL' or 'lastModifiedBy' JSON facet\n\tvoid queryOneDriveForFileDetails(string inputFilePath, string runtimePath, string outputType) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tOneDriveApi queryOneDriveForFileDetailsApiInstance;\n\t\t\n\t\t// Calculate the full local file path\n\t\tstring fullLocalFilePath = buildNormalizedPath(buildPath(runtimePath, inputFilePath));\n\t\t\n\t\t// Query if file is valid locally\n\t\tif (exists(fullLocalFilePath)) {\n\t\t\t// search drive_id list\n\t\t\tstring[] distinctDriveIds = itemDB.selectDistinctDriveIds();\n\t\t\tbool pathInDB = false;\n\t\t\tItem dbItem;\n\t\t\t\n\t\t\tforeach (searchDriveId; distinctDriveIds) {\n\t\t\t\t// Does this path exist in the database, use the 'inputFilePath'\n\t\t\t\tif (itemDB.selectByPath(inputFilePath, searchDriveId, dbItem)) {\n\t\t\t\t\t// item is in the database\n\t\t\t\t\tpathInDB = true;\n\t\t\t\t\tJSONValue fileDetailsFromOneDrive;\n\t\t\t\t\n\t\t\t\t\t// Create a new API Instance for this thread and initialise it\n\t\t\t\t\tqueryOneDriveForFileDetailsApiInstance = new OneDriveApi(appConfig);\n\t\t\t\t\tqueryOneDriveForFileDetailsApiInstance.initialise();\n\t\t\t\t\t\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfileDetailsFromOneDrive = queryOneDriveForFileDetailsApiInstance.getPathDetailsById(dbItem.driveId, dbItem.id);\n\t\t\t\t\t\t// Dont cleanup here as if we are creating a shareable file link (below) it is still needed\n\t\t\t\t\t\t\n\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\t// display what the error is\n\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t\n\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\tqueryOneDriveForFileDetailsApiInstance.releaseCurlEngine();\n\t\t\t\t\t\tqueryOneDriveForFileDetailsApiInstance = null;\n\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Is the API response a valid JSON file?\n\t\t\t\t\tif (fileDetailsFromOneDrive.type() == JSONType.object) {\n\t\t\t\t\t\n\t\t\t\t\t\t// debug output of response\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"API Response: \" ~ to!string(fileDetailsFromOneDrive), [\"debug\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// What sort of response to we generate\n\t\t\t\t\t\t// --get-file-link response\n\t\t\t\t\t\tif (outputType == \"URL\") {\n\t\t\t\t\t\t\tif ((fileDetailsFromOneDrive.type() == JSONType.object) && (\"webUrl\" in fileDetailsFromOneDrive)) {\n\t\t\t\t\t\t\t\t// Valid JSON object\n\t\t\t\t\t\t\t\taddLogEntry();\n\t\t\t\t\t\t\t\twriteln(\"WebURL: \", fileDetailsFromOneDrive[\"webUrl\"].str);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// --modified-by response\n\t\t\t\t\t\tif (outputType == \"ModifiedBy\") {\n\t\t\t\t\t\t\tif ((fileDetailsFromOneDrive.type() == JSONType.object) && (\"lastModifiedBy\" in fileDetailsFromOneDrive)) {\n\t\t\t\t\t\t\t\t// Valid JSON object\n\t\t\t\t\t\t\t\twriteln();\n\t\t\t\t\t\t\t\twriteln(\"Last modified:    \", fileDetailsFromOneDrive[\"lastModifiedDateTime\"].str);\n\t\t\t\t\t\t\t\twriteln(\"Last modified by: \", fileDetailsFromOneDrive[\"lastModifiedBy\"][\"user\"][\"displayName\"].str);\n\t\t\t\t\t\t\t\t// if 'email' provided, add this to the output\n\t\t\t\t\t\t\t\tif (\"email\" in fileDetailsFromOneDrive[\"lastModifiedBy\"][\"user\"]) {\n\t\t\t\t\t\t\t\t\twriteln(\"Email Address:    \", fileDetailsFromOneDrive[\"lastModifiedBy\"][\"user\"][\"email\"].str);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// --create-share-link response\n\t\t\t\t\t\tif (outputType == \"ShareableLink\") {\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tJSONValue accessScope;\n\t\t\t\t\t\t\tJSONValue createShareableLinkResponse;\n\t\t\t\t\t\t\tstring thisDriveId = fileDetailsFromOneDrive[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\t\t\tstring thisItemId = fileDetailsFromOneDrive[\"id\"].str;\n\t\t\t\t\t\t\tstring fileShareLink;\n\t\t\t\t\t\t\tbool writeablePermissions = appConfig.getValueBool(\"with_editing_perms\");\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// What sort of shareable link is required?\n\t\t\t\t\t\t\tif (writeablePermissions) {\n\t\t\t\t\t\t\t\t// configure the read-write access scope\n\t\t\t\t\t\t\t\taccessScope = [\n\t\t\t\t\t\t\t\t\t\"type\": \"edit\",\n\t\t\t\t\t\t\t\t\t\"scope\": \"anonymous\"\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// configure the read-only access scope (default)\n\t\t\t\t\t\t\t\taccessScope = [\n\t\t\t\t\t\t\t\t\t\"type\": \"view\",\n\t\t\t\t\t\t\t\t\t\"scope\": \"anonymous\"\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// If a share-password was passed use it when creating the link \n\t\t\t\t\t\t\tif (strip(appConfig.getValueString(\"share_password\")) != \"\") {\n                                                                accessScope[\"password\"] = appConfig.getValueString(\"share_password\");\n                                                        }\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Try and create the shareable file link\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tcreateShareableLinkResponse = queryOneDriveForFileDetailsApiInstance.createShareableLink(thisDriveId, thisItemId, accessScope);\n\t\t\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\t\t\t// display what the error is\n\t\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Is the API response a valid JSON file?\n\t\t\t\t\t\t\tif ((createShareableLinkResponse.type() == JSONType.object) && (\"link\" in createShareableLinkResponse)) {\n\t\t\t\t\t\t\t\t// Extract the file share link from the JSON response\n\t\t\t\t\t\t\t\tfileShareLink = createShareableLinkResponse[\"link\"][\"webUrl\"].str;\n\t\t\t\t\t\t\t\twriteln(\"File Shareable Link: \", fileShareLink);\n\t\t\t\t\t\t\t\tif (writeablePermissions) {\n\t\t\t\t\t\t\t\t\twriteln(\"Shareable Link has read-write permissions - use and provide with caution\"); \n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\tqueryOneDriveForFileDetailsApiInstance.releaseCurlEngine();\n\t\t\t\t\tqueryOneDriveForFileDetailsApiInstance = null;\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// was path found?\n\t\t\tif (!pathInDB) {\n\t\t\t\t// File has not been synced with OneDrive\n\t\t\t\taddLogEntry(\"Selected path has not been synced with Microsoft OneDrive: \" ~ inputFilePath);\n\t\t\t}\n\t\t} else {\n\t\t\t// File does not exist locally\n\t\t\taddLogEntry(\"Selected path not found on local system: \" ~ inputFilePath);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Query OneDrive for the quota details\n\tvoid queryOneDriveForQuotaDetails() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// This function is similar to getRemainingFreeSpace() but is different in data being analysed and output method\n\t\tJSONValue currentDriveQuota;\n\t\tstring driveId;\n\t\tOneDriveApi getCurrentDriveQuotaApiInstance;\n\n\t\tif (appConfig.getValueString(\"drive_id\").length) {\n\t\t\tdriveId = appConfig.getValueString(\"drive_id\");\n\t\t} else {\n\t\t\tdriveId = appConfig.defaultDriveId;\n\t\t}\n\t\t\n\t\ttry {\n\t\t\t// Create a new OneDrive API instance\n\t\t\tgetCurrentDriveQuotaApiInstance = new OneDriveApi(appConfig);\n\t\t\tgetCurrentDriveQuotaApiInstance.initialise();\n\t\t\tif (debugLogging) {addLogEntry(\"Seeking available quota for this drive id: \" ~ driveId, [\"debug\"]);}\n\t\t\tcurrentDriveQuota = getCurrentDriveQuotaApiInstance.getDriveQuota(driveId);\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tgetCurrentDriveQuotaApiInstance.releaseCurlEngine();\n\t\t\tgetCurrentDriveQuotaApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t} catch (OneDriveException e) {\n\t\t\tif (debugLogging) {addLogEntry(\"currentDriveQuota = onedrive.getDriveQuota(driveId) generated a OneDriveException\", [\"debug\"]);}\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tgetCurrentDriveQuotaApiInstance.releaseCurlEngine();\n\t\t\tgetCurrentDriveQuotaApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t}\n\t\t\n\t\t// validate that currentDriveQuota is a JSON value\n\t\tif (currentDriveQuota.type() == JSONType.object) {\n\t\t\t// was 'quota' in response?\n\t\t\tif (\"quota\" in currentDriveQuota) {\n\t\t\n\t\t\t\t// debug output of response\n\t\t\t\tif (debugLogging) {addLogEntry(\"currentDriveQuota: \" ~ to!string(currentDriveQuota), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// human readable output of response\n\t\t\t\tstring deletedValue = \"Not Provided\";\n\t\t\t\tstring remainingValue = \"Not Provided\";\n\t\t\t\tstring stateValue = \"Not Provided\";\n\t\t\t\tstring totalValue = \"Not Provided\";\n\t\t\t\tstring usedValue = \"Not Provided\";\n\t\t\t\n\t\t\t\t// Update values\n\t\t\t\tif (\"deleted\" in currentDriveQuota[\"quota\"]) {\n\t\t\t\t\tdeletedValue = byteToGibiByte(currentDriveQuota[\"quota\"][\"deleted\"].integer);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (\"remaining\" in currentDriveQuota[\"quota\"]) {\n\t\t\t\t\tremainingValue = byteToGibiByte(currentDriveQuota[\"quota\"][\"remaining\"].integer);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (\"state\" in currentDriveQuota[\"quota\"]) {\n\t\t\t\t\tstateValue = currentDriveQuota[\"quota\"][\"state\"].str;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (\"total\" in currentDriveQuota[\"quota\"]) {\n\t\t\t\t\ttotalValue = byteToGibiByte(currentDriveQuota[\"quota\"][\"total\"].integer);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (\"used\" in currentDriveQuota[\"quota\"]) {\n\t\t\t\t\tusedValue = byteToGibiByte(currentDriveQuota[\"quota\"][\"used\"].integer);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\twriteln(\"Microsoft OneDrive quota information as reported for this Drive ID: \", driveId);\n\t\t\t\twriteln();\n\t\t\t\twriteln(\"Deleted:   \", deletedValue, \" GB (\", currentDriveQuota[\"quota\"][\"deleted\"].integer, \" bytes)\");\n\t\t\t\twriteln(\"Remaining: \", remainingValue, \" GB (\", currentDriveQuota[\"quota\"][\"remaining\"].integer, \" bytes)\");\n\t\t\t\twriteln(\"State:     \", stateValue);\n\t\t\t\twriteln(\"Total:     \", totalValue, \" GB (\", currentDriveQuota[\"quota\"][\"total\"].integer, \" bytes)\");\n\t\t\t\twriteln(\"Used:      \", usedValue, \" GB (\", currentDriveQuota[\"quota\"][\"used\"].integer, \" bytes)\");\n\t\t\t\twriteln();\n\t\t\t} else {\n\t\t\t\twriteln(\"Microsoft OneDrive quota information is being restricted for this Drive ID: \", driveId);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Query the system for session_upload.* files\n\tbool checkForInterruptedSessionUploads() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tbool interruptedUploads = false;\n\t\tlong interruptedUploadsCount;\n\t\t\n\t\t// Scan the filesystem for the files we are interested in, build up interruptedUploadsSessionFiles array\n\t\tforeach (sessionFile; dirEntries(appConfig.configDirName, \"session_upload.*\", SpanMode.shallow)) {\n\t\t\t// calculate the full path\n\t\t\tstring tempPath = buildNormalizedPath(buildPath(appConfig.configDirName, sessionFile));\n\t\t\t// add to array\n\t\t\tinterruptedUploadsSessionFiles ~= [tempPath];\n\t\t}\n\t\t\n\t\t// Count all 'session_upload' files in appConfig.configDirName\n\t\tinterruptedUploadsCount = count(interruptedUploadsSessionFiles);\n\t\tif (interruptedUploadsCount != 0) {\n\t\t\tinterruptedUploads = true;\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return if there are interrupted uploads to process\n\t\treturn interruptedUploads;\n\t}\n\t\n\t// Query the system for resume_download.* files\n\tbool checkForResumableDownloads() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tbool resumableDownloads = false;\n\t\tlong resumableDownloadsCount;\n\t\t\n\t\t// Scan the filesystem for the files we are interested in, build up interruptedDownloadFiles array\n\t\tforeach (resumeDownloadFile; dirEntries(appConfig.configDirName, \"resume_download.*\", SpanMode.shallow)) {\n\t\t\t// calculate the full path\n\t\t\tstring tempPath = buildNormalizedPath(buildPath(appConfig.configDirName, resumeDownloadFile));\n\t\t\t// add to array\n\t\t\tinterruptedDownloadFiles ~= [tempPath];\n\t\t}\n\t\t\n\t\t// Count all 'resume_download' files in appConfig.configDirName\n\t\tresumableDownloadsCount = count(interruptedDownloadFiles);\n\t\tif (resumableDownloadsCount != 0) {\n\t\t\tresumableDownloads = true;\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return if there are interrupted uploads to process\n\t\treturn resumableDownloads;\n\t}\n\t\n\t// Clear any session_upload.* files\n\tvoid clearInterruptedSessionUploads() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Scan the filesystem for the files we are interested in, build up interruptedUploadsSessionFiles array\n\t\tforeach (sessionFile; dirEntries(appConfig.configDirName, \"session_upload.*\", SpanMode.shallow)) {\n\t\t\t// calculate the full path\n\t\t\tstring tempPath = buildNormalizedPath(buildPath(appConfig.configDirName, sessionFile));\n\t\t\tJSONValue sessionFileData = readText(tempPath).parseJSON();\n\t\t\taddLogEntry(\"Removing interrupted session upload file due to --resync for: \" ~ sessionFileData[\"localPath\"].str, [\"info\"]);\n\t\t\t\n\t\t\t// Process removal\n\t\t\tif (!dryRun) {\n\t\t\t\tsafeRemove(tempPath);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Clear any resume_download.* files\n\tvoid clearInterruptedDownloads() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Scan the filesystem for the files we are interested in, build up interruptedDownloadFiles array\n\t\tforeach (resumeDownloadFile; dirEntries(appConfig.configDirName, \"resume_download.*\", SpanMode.shallow)) {\n\t\t\t// calculate the full path\n\t\t\tstring tempPath = buildNormalizedPath(buildPath(appConfig.configDirName, resumeDownloadFile));\n\t\t\t\n\t\t\t\n\t\t\tJSONValue resumeFileData = readText(tempPath).parseJSON();\n\t\t\taddLogEntry(\"Removing interrupted download file due to --resync for: \" ~ resumeFileData[\"originalFilename\"].str, [\"info\"]);\n\t\t\tstring resumeFilename = resumeFileData[\"downloadFilename\"].str;\n\t\t\t\n\t\t\t// Process removal\n\t\t\tif (!dryRun) {\n\t\t\t\t// remove the .partial file\n\t\t\t\tsafeRemove(resumeFilename);\n\t\t\t\t// remove the resume_download. file\n\t\t\t\tsafeRemove(tempPath);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Process interrupted 'session_upload' files\n\tvoid processInterruptedSessionUploads() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// For each upload_session file that has been found, process the data to ensure it is still valid\n\t\tforeach (sessionFilePath; interruptedUploadsSessionFiles) {\n\t\t\t// What session data are we trying to restore\n\t\t\tif (verboseLogging) {addLogEntry(\"Attempting to restore file upload session using this session data file: \" ~ sessionFilePath, [\"verbose\"]);}\n\t\t\t// Does this pass validation?\n\t\t\tif (!validateUploadSessionFileData(sessionFilePath)) {\n\t\t\t\t// Remove upload_session file as it is invalid\n\t\t\t\t// upload_session file contains an error - cant resume this session\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Restore file upload session failed - cleaning up resumable session data file: \" ~ sessionFilePath, [\"verbose\"]);}\n\t\t\t\t\n\t\t\t\t// cleanup session path\n\t\t\t\tif (exists(sessionFilePath)) {\n\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\tsafeRemove(sessionFilePath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// At this point we should have an array of JSON items to resume uploading\n\t\tif (count(jsonItemsToResumeUpload) > 0) {\n\t\t\t// there are valid items to resume upload\n\t\t\t// Lets deal with all the JSON items that need to be resumed for upload in a batch process\n\t\t\tsize_t batchSize = to!int(appConfig.getValueLong(\"threads\"));\n\t\t\tlong batchCount = (jsonItemsToResumeUpload.length + batchSize - 1) / batchSize;\n\t\t\tlong batchesProcessed = 0;\n\t\t\t\n\t\t\tforeach (chunk; jsonItemsToResumeUpload.chunks(batchSize)) {\n\t\t\t\t// send an array containing 'appConfig.getValueLong(\"threads\")' JSON items to resume upload\n\t\t\t\tresumeSessionUploadsInParallel(chunk);\n\t\t\t}\n\t\t\t\n\t\t\t// For this set of items, perform a DB PASSIVE checkpoint\n\t\t\titemDB.performCheckpoint(\"PASSIVE\");\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Process 'resumable download' files that were found\n\tvoid processResumableDownloadFiles() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// For each 'resume_download' file that has been found, process the data to ensure it is still valid\n\t\tforeach (resumeDownloadFile; interruptedDownloadFiles) {\n\t\t\t// What 'resumable data' are we trying to resume\n\t\t\tif (verboseLogging) {addLogEntry(\"Attempting to resume file download using this 'resumable data' file: \" ~ resumeDownloadFile, [\"verbose\"]);}\n\t\t\t// Does this pass validation?\n\t\t\tif (!validateResumableDownloadFileData(resumeDownloadFile)) {\n\t\t\t\t// Remove 'resume_download' file as it is invalid\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Resume file download verification failed - cleaning up resumable download data file: \" ~ resumeDownloadFile, [\"verbose\"]);}\n\t\t\t\t// Cleanup 'resume_download' file\n\t\t\t\tif (exists(resumeDownloadFile)) {\n\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\tsafeRemove(resumeDownloadFile);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// At this point we should have an array of JSON items to resume downloading\n\t\tif (count(jsonItemsToResumeDownload) > 0) {\n\t\t\t// There are valid items to resume download\n\t\t\t// Lets deal with all the JSON items that need to be resumed for download in a batch process\n\t\t\tsize_t batchSize = to!int(appConfig.getValueLong(\"threads\"));\n\t\t\tlong batchCount = (jsonItemsToResumeDownload.length + batchSize - 1) / batchSize;\n\t\t\tlong batchesProcessed = 0;\n\t\t\t\n\t\t\tforeach (chunk; jsonItemsToResumeDownload.chunks(batchSize)) {\n\t\t\t\t// send an array containing 'appConfig.getValueLong(\"threads\")' JSON items to resume download\n\t\t\t\tresumeDownloadsInParallel(chunk);\n\t\t\t}\n\t\t\t\n\t\t\t// For this set of items, perform a DB PASSIVE checkpoint\n\t\t\titemDB.performCheckpoint(\"PASSIVE\");\n\t\t}\n\t\t\n\t\t// Cleanup all 'resume_download' files\n\t\tforeach (resumeDownloadFile; interruptedDownloadFiles) {\n\t\t\tsafeRemove(resumeDownloadFile);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// A resume session upload file needs to be valid to be used\n\t// This function validates this data\n\tbool validateUploadSessionFileData(string sessionFilePath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Due to this function, we need to keep the return <bool value>; code, so that this function operates as efficiently as possible.\n\t\t// It is pointless having the entire code run through and performing additional needless checks where it is not required\n\t\t// Whilst this means some extra code / duplication in this function, it cannot be helped\n\t\t\n\t\tJSONValue sessionFileData;\n\t\tOneDriveApi validateUploadSessionFileDataApiInstance;\n\n\t\t// Try and read the text from the session file as a JSON array\n\t\ttry {\n\t\t\tif (getSize(sessionFilePath) > 0) {\n\t\t\t\t// There is data to read in\n\t\t\t\tsessionFileData = readText(sessionFilePath).parseJSON();\n\t\t\t} else {\n\t\t\t\t// No data to read in - invalid file\n\t\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: Invalid JSON file: \" ~ sessionFilePath, [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// return session file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch (JSONException e) {\n\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: Invalid JSON data in: \" ~ sessionFilePath, [\"debug\"]);}\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\t// return session file is invalid\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// Does the file we wish to resume uploading exist locally still?\n\t\tif (\"localPath\" in sessionFileData) {\n\t\t\tstring sessionLocalFilePath = sessionFileData[\"localPath\"].str;\n\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: sessionLocalFilePath: \" ~ sessionLocalFilePath, [\"debug\"]);}\n\t\t\t\n\t\t\t// Does the file exist?\n\t\t\tif (!exists(sessionLocalFilePath)) {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"The local file to upload does not exist locally anymore\", [\"verbose\"]);}\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// return session file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t\n\t\t\t// Can we read the file?\n\t\t\tif (!readLocalFile(sessionLocalFilePath)) {\n\t\t\t\t// filesystem error already returned if unable to read\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// return session file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t\n\t\t} else {\n\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: No localPath data in: \" ~ sessionFilePath, [\"debug\"]);}\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\t// return session file is invalid\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// Check the session data for expirationDateTime\n\t\tif (\"expirationDateTime\" in sessionFileData) {\n\t\t\tSysTime expiration;\n\t\t\tstring expirationTimestamp;\n\t\t\texpirationTimestamp = strip(sessionFileData[\"expirationDateTime\"].str);\n\t\t\t\n\t\t\t// is expirationTimestamp valid?\n\t\t\tif (isValidUTCDateTime(expirationTimestamp)) {\n\t\t\t\t// string is a valid timestamp\n\t\t\t\texpiration = SysTime.fromISOExtString(expirationTimestamp);\n\t\t\t} else {\n\t\t\t\t// invalid timestamp from JSON file\n\t\t\t\taddLogEntry(\"WARNING: Invalid timestamp provided by the Microsoft OneDrive API: \" ~ expirationTimestamp);\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// return session file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t\n\t\t\t// valid timestamp\n\t\t\tif (expiration < Clock.currTime()) {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"The upload session has expired for: \" ~ sessionFilePath, [\"verbose\"]);}\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// return session file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: No expirationDateTime data in: \" ~ sessionFilePath, [\"debug\"]);}\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\t// return session file is invalid\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// Check the online upload status, using the uloadURL in sessionFileData\n\t\tif (\"uploadUrl\" in sessionFileData) {\n\t\t\tJSONValue response;\n\t\t\t\n\t\t\ttry {\n\t\t\t\t// Create a new OneDrive API instance\n\t\t\t\tvalidateUploadSessionFileDataApiInstance = new OneDriveApi(appConfig);\n\t\t\t\tvalidateUploadSessionFileDataApiInstance.initialise();\n\t\t\t\t\n\t\t\t\t// Request upload status\n\t\t\t\tresponse = validateUploadSessionFileDataApiInstance.requestUploadStatus(sessionFileData[\"uploadUrl\"].str);\n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tvalidateUploadSessionFileDataApiInstance.releaseCurlEngine();\n\t\t\t\tvalidateUploadSessionFileDataApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// no error .. potentially all still valid\n\t\t\t\t\n\t\t\t} catch (OneDriveException e) {\n\t\t\t\t// handle any onedrive error response as invalid\n\t\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: Invalid response when using uploadUrl in: \" ~ sessionFilePath, [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\tvalidateUploadSessionFileDataApiInstance.releaseCurlEngine();\n\t\t\t\tvalidateUploadSessionFileDataApiInstance = null;\n\t\t\t\t// Perform Garbage Collection\n\t\t\t\tGC.collect();\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// return session file is invalid\n\t\t\t\treturn false;\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\t// Do we have a valid response from OneDrive?\n\t\t\tif (response.type() == JSONType.object) {\n\t\t\t\t// Valid JSON object was returned\n\t\t\t\tif ((\"expirationDateTime\" in response) && (\"nextExpectedRanges\" in response)) {\n\t\t\t\t\t// The 'uploadUrl' is valid, and the response contains elements we need\n\t\t\t\t\tsessionFileData[\"expirationDateTime\"] = response[\"expirationDateTime\"];\n\t\t\t\t\tsessionFileData[\"nextExpectedRanges\"] = response[\"nextExpectedRanges\"];\n\t\t\t\t\t\n\t\t\t\t\tif (sessionFileData[\"nextExpectedRanges\"].array.length == 0) {\n\t\t\t\t\t\tif (verboseLogging) {addLogEntry(\"The upload session was already completed\", [\"verbose\"]);}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// return session file is invalid\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: No expirationDateTime & nextExpectedRanges data in Microsoft OneDrive API response: \" ~ to!string(response), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// return session file is invalid\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// not a JSON object\n\t\t\t\tif (verboseLogging) {addLogEntry(\"Restore file upload session failed - invalid response from Microsoft OneDrive\", [\"verbose\"]);}\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// return session file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: No uploadUrl data in: \" ~ sessionFilePath, [\"debug\"]);}\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\t// return session file is invalid\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// Add 'sessionFilePath' to 'sessionFileData' so that it can be used when we reuse the JSON data to resume the upload\n\t\tsessionFileData[\"sessionFilePath\"] = sessionFilePath;\n\t\t\n\t\t// Add sessionFileData to jsonItemsToResumeUpload as it is now valid\n\t\tjsonItemsToResumeUpload ~= sessionFileData;\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return session file is valid\n\t\treturn true;\n\t}\n\t\n\t// A 'resumable download' file needs to be valid to be used\n\tbool validateResumableDownloadFileData(string resumeDownloadFile) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Function variables\n\t\tJSONValue resumeDownloadFileData;\n\t\tJSONValue latestOnlineFileDetails;\n\t\tOneDriveApi validateResumableDownloadFileDataApiInstance;\n\t\tstring driveId;\n\t\tstring itemId;\n\t\tstring existingHash;\n\t\tstring downloadFilename;\n\t\tlong resumeOffset;\n\t\tstring OneDriveFileXORHash;\n\t\tstring OneDriveFileSHA256Hash;\n\t\t\n\t\t// Try and read the text from the 'resumable download' file as a JSON array\n\t\ttry {\n\t\t\tif (getSize(resumeDownloadFile) > 0) {\n\t\t\t\t// There is data to read in\n\t\t\t\tresumeDownloadFileData = readText(resumeDownloadFile).parseJSON();\n\t\t\t} else {\n\t\t\t\t// No data to read in - invalid file\n\t\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: Invalid JSON file: \" ~ resumeDownloadFile, [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Return 'resumable download' file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch (JSONException e) {\n\t\t\tif (debugLogging) {addLogEntry(\"SESSION-RESUME: Invalid JSON data in: \" ~ resumeDownloadFile, [\"debug\"]);}\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\t// Return 'resumable download' file is invalid\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// What needs to be checked?\n\t\t// - JSON has 'downloadFilename' - critical to check the online state\n\t\t// - JSON has 'driveId' - critical to check the online state\n\t\t// - JSON has 'itemId'  - critical to check the online state\n\t\t// - JSON has 'resumeOffset' - critical to check the online state\n\t\t// - JSON has 'onlineHash' with an applicable hash value - critical to check the online state\n\t\t\n\t\tif (!hasDownloadFilename(resumeDownloadFileData)) {\n\t\t\t// no downloadFilename present - file invalid\n\t\t\tif (verboseLogging) {addLogEntry(\"The 'resumable download' file contains invalid data: Missing 'downloadFilename'\", [\"verbose\"]);}\n\t\t\t// Return 'resumable download' file is invalid\n\t\t\treturn false;\n\t\t} else {\n\t\t\t// Configure search variables\n\t\t\tdownloadFilename = resumeDownloadFileData[\"downloadFilename\"].str;\n\t\t\t// Does the file specified by 'downloadFilename' exist on disk?\n\t\t\tif (!exists(downloadFilename)) {\n\t\t\t\t// File that is supposed to contain our resumable \n\t\t\t\tif (verboseLogging) {addLogEntry(\"The 'resumable download' file no longer exists on your local disk: \" ~ downloadFilename, [\"verbose\"]);}\n\t\t\t\t// Return 'resumable download' file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// If we get to this point 'downloadFilename' has a file name and the file exists on disk.\n\t\t// If any of the other validations fail, we can remove the file\n\t\t\n\t\tif (!hasDriveId(resumeDownloadFileData)) {\n\t\t\t// no driveId present - file invalid\n\t\t\tif (verboseLogging) {addLogEntry(\"The 'resumable download' file contains invalid data: Missing 'driveId'\", [\"verbose\"]);}\n\t\t\t// Remove local file\n\t\t\tsafeRemove(downloadFilename);\n\t\t\t// Return 'resumable download' file is invalid\n\t\t\treturn false;\n\t\t} else {\n\t\t\t// Configure search variables\n\t\t\tdriveId = resumeDownloadFileData[\"driveId\"].str;\n\t\t}\n\t\t\n\t\tif (!hasItemId(resumeDownloadFileData)) {\n\t\t\t// no itemId present - file invalid\n\t\t\tif (verboseLogging) {addLogEntry(\"The 'resumable download' file contains invalid data: Missing 'itemId'\", [\"verbose\"]);}\n\t\t\t// Remove local file\n\t\t\tsafeRemove(downloadFilename);\n\t\t\t// Return 'resumable download' file is invalid\n\t\t\treturn false;\n\t\t} else {\n\t\t\t// Configure search variables\n\t\t\titemId = resumeDownloadFileData[\"itemId\"].str;\n\t\t}\n\t\t\n\t\tif (!hasResumeOffset(resumeDownloadFileData)) {\n\t\t\t// no resumeOffset present - file invalid\n\t\t\tif (verboseLogging) {addLogEntry(\"The 'resumable download' file contains invalid data: Missing 'resumeOffset'\", [\"verbose\"]);}\n\t\t\t// Remove local file\n\t\t\tsafeRemove(downloadFilename);\n\t\t\t// Return 'resumable download' file is invalid\n\t\t\treturn false;\n\t\t} else {\n\t\t\t// we have a resumeOffset value\n\t\t\tresumeOffset = to!long(resumeDownloadFileData[\"resumeOffset\"].str);\n\t\t\t// We need to check 'resumeOffset' against the 'downloadFilename' on-disk size\n\t\t\tlong onDiskSize = getSize(downloadFilename);\n\t\t\t\n\t\t\tif (resumeOffset != onDiskSize) {\n\t\t\t\t// The size of the offset location does not equal the size on disk .. if we resume that file, the file will be corrupt\n\t\t\t\tstring logMessage = format(\"The 'resumable download' file on disk is a different size to the resumable offset: %s vs %s\", to!string(resumeOffset), to!string(onDiskSize));\n\t\t\t\tif (verboseLogging) {addLogEntry(logMessage, [\"verbose\"]);}\n\t\t\t\t// Remove local file\n\t\t\t\tsafeRemove(downloadFilename);\n\t\t\t\t// Return 'resumable download' file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (!hasOnlineHash(resumeDownloadFileData)) {\n\t\t\t// no onlineHash present - file invalid\n\t\t\tif (verboseLogging) {addLogEntry(\"The 'resumable download' file contains invalid data: Missing 'onlineHash'\", [\"verbose\"]);}\n\t\t\t// Remove local file\n\t\t\tsafeRemove(downloadFilename);\n\t\t\t// Return 'resumable download' file is invalid\n\t\t\treturn false;\n\t\t} else {\n\t\t\t// Configure hash variable from the resume data\n\t\t\t// QuickXorHash Check\n\t\t\tif (hasQuickXorHashResume(resumeDownloadFileData)) {\n\t\t\t\t// We have a quickXorHash value\n\t\t\t\texistingHash = resumeDownloadFileData[\"onlineHash\"][\"quickXorHash\"].str;\n\t\t\t} else {\n\t\t\t\t// Fallback: Check for SHA256Hash\n\t\t\t\tif (hasSHA256HashResume(resumeDownloadFileData)) {\n\t\t\t\t\t// We have a sha256Hash value\n\t\t\t\t\texistingHash = resumeDownloadFileData[\"onlineHash\"][\"sha256Hash\"].str;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// At this point if we do not have a existingHash value, its a fail\n\t\t\tif (existingHash.empty) {\n\t\t\t\tif (verboseLogging) {addLogEntry(\"The 'resumable download' file contains invalid data: Missing 'onlineHash' value\", [\"verbose\"]);}\n\t\t\t\t// Remove local file\n\t\t\t\tsafeRemove(downloadFilename);\n\t\t\t\t// Return 'resumable download' file is invalid\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\t\t\t\n\t\t// At this point we have elements in the 'resumable download' JSON data that will allow is to check if the online file has been modified - if it has, resuming the download is pointless\n\t\ttry {\n\t\t\t// Create a new OneDrive API instance\n\t\t\tvalidateResumableDownloadFileDataApiInstance = new OneDriveApi(appConfig);\n\t\t\tvalidateResumableDownloadFileDataApiInstance.initialise();\n\t\n\t\t\t// Request latest file details\n\t\t\tlatestOnlineFileDetails = validateResumableDownloadFileDataApiInstance.getPathDetailsById(driveId, itemId);\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tvalidateResumableDownloadFileDataApiInstance.releaseCurlEngine();\n\t\t\tvalidateResumableDownloadFileDataApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t\t// no error .. potentially all still valid\n\t\t} catch (OneDriveException e) {\n\t\t\t// handle any onedrive error response as invalid\n\t\t\t\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tvalidateResumableDownloadFileDataApiInstance.releaseCurlEngine();\n\t\t\tvalidateResumableDownloadFileDataApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\t\n\t\t\t// Return 'resumable download' file is invalid\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// Configure the hashes from the online data for comparison\n\t\tif (hasHashes(latestOnlineFileDetails)) {\n\t\t\t// File details returned hash details\n\t\t\t// QuickXorHash\n\t\t\tif (hasQuickXorHash(latestOnlineFileDetails)) {\n\t\t\t\t// Use the provided quickXorHash as reported by OneDrive\n\t\t\t\tif (latestOnlineFileDetails[\"file\"][\"hashes\"][\"quickXorHash\"].str != \"\") {\n\t\t\t\t\tOneDriveFileXORHash = latestOnlineFileDetails[\"file\"][\"hashes\"][\"quickXorHash\"].str;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Fallback: Check for SHA256Hash\n\t\t\t\tif (hasSHA256Hash(latestOnlineFileDetails)) {\n\t\t\t\t\t// Use the provided sha256Hash as reported by OneDrive\n\t\t\t\t\tif (latestOnlineFileDetails[\"file\"][\"hashes\"][\"sha256Hash\"].str != \"\") {\n\t\t\t\t\t\tOneDriveFileSHA256Hash = latestOnlineFileDetails[\"file\"][\"hashes\"][\"sha256Hash\"].str;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Last check - has the online file changed since we attempted to do the download that we are trying to resume?\n\t\t// Test 'existingHash' against the potential 2 online hashes for a match\n\t\t// As we dont know what type of hash 'existingHash' is, we have to test it against the 2 known online types\n\t\tbool hashesMatch = (existingHash == OneDriveFileXORHash) || (existingHash == OneDriveFileSHA256Hash);\n\t\t\n\t\t// Do the hashes match?\n\t\tif (!hashesMatch) {\n\t\t\t// Hashes do not match\n\t\t\tif (verboseLogging) {addLogEntry(\"The 'online file' has changed in content since the download was last attempted. Aborting this resumable download attempt.\", [\"verbose\"]);}\n\t\t\t// Remove local file\n\t\t\tsafeRemove(downloadFilename);\n\t\t\t// Return 'resumable download' file is invalid\n\t\t\treturn false;\t\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Augment 'latestOnlineFileDetails' with our resume point\n\t\tlatestOnlineFileDetails[\"resumeOffset\"] = JSONValue(to!string(resumeOffset));\n\t\t\n\t\t// Add latestOnlineFileDetails to jsonItemsToResumeDownload as it is now valid\n\t\tjsonItemsToResumeDownload ~= latestOnlineFileDetails;\n\t\t\n\t\t// Return 'resumable download' file is valid\n\t\treturn true;\n\t}\n\t\n\t// Resume all resumable session uploads in parallel\n\tvoid resumeSessionUploadsInParallel(JSONValue[] array) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// This function received an array of JSON items to resume upload, the number of elements based on appConfig.getValueLong(\"threads\")\n\t\tforeach (i, jsonItemToResume; processPool.parallel(array)) {\n\t\t\t// Take each JSON item and resume upload using the JSON data\n\t\t\tJSONValue uploadResponse;\n\t\t\tOneDriveApi uploadFileOneDriveApiInstance;\n\t\t\t\n\t\t\t// Create a new API instance\n\t\t\tuploadFileOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\t\tuploadFileOneDriveApiInstance.initialise();\n\t\t\t\n\t\t\t// Pull out data from this JSON element\n\t\t\tstring threadUploadSessionFilePath = jsonItemToResume[\"sessionFilePath\"].str;\n\t\t\tlong thisFileSizeLocal = getSize(jsonItemToResume[\"localPath\"].str);\n\t\t\t\n\t\t\t// Try to resume the session upload using the provided data\n\t\t\ttry {\n\t\t\t\tuploadResponse = performSessionFileUpload(uploadFileOneDriveApiInstance, thisFileSizeLocal, jsonItemToResume, threadUploadSessionFilePath);\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\twriteln(\"CODING TO DO: Handle an exception when performing a resume session upload\");\t\n\t\t\t}\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tuploadFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\tuploadFileOneDriveApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\t\t\t\n\t\t\t// Was the response from the OneDrive API a valid JSON item?\n\t\t\tif (uploadResponse.type() == JSONType.object) {\n\t\t\t\t// A valid JSON object was returned - session resumption upload successful\n\t\t\t\t\n\t\t\t\t// Are we in an --upload-only & --remove-source-files scenario?\n\t\t\t\t// Use actual config values as we are doing an upload session recovery\n\t\t\t\tif ((uploadOnly) && (localDeleteAfterUpload)) {\n\t\t\t\t\t// Perform the local file deletion\n\t\t\t\t\tremoveLocalFilePostUpload(jsonItemToResume[\"localPath\"].str);\n\t\t\t\t\t\n\t\t\t\t\t// as file is removed, we have nothing to add to the local database\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Skipping adding to database as --upload-only & --remove-source-files configured\", [\"debug\"]);}\n\t\t\t\t} else {\n\t\t\t\t\t// Save JSON item in database\n\t\t\t\t\tsaveItem(uploadResponse);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No valid response was returned\n\t\t\t\taddLogEntry(\"CODING TO DO: what to do when session upload resumption JSON data is not valid ... nothing ? error message ?\");\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Resume all resumable downloads in parallel\n\tvoid resumeDownloadsInParallel(JSONValue[] array) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// This function received an array of JSON items to resume download, the number of elements based on appConfig.getValueLong(\"threads\")\n\t\tforeach (i, jsonItemToResume; processPool.parallel(array)) {\n\t\t\t// Take each JSON item and resume download using the JSON data\n\t\t\t\n\t\t\t// Extract the 'offset' from the JSON data\n\t\t\tlong resumeOffset;\n\t\t\tresumeOffset = to!long(jsonItemToResume[\"resumeOffset\"].str);\n\t\t\t\n\t\t\t// Take each JSON item and download it using the offset\n\t\t\tdownloadFileItem(jsonItemToResume, false, resumeOffset);\n\t\t}\n\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Function to process the path by removing prefix up to ':' - remove '/drive/root:' from a path string\n\tstring processPathToRemoveRootReference(ref string pathToCheck) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tsize_t colonIndex = pathToCheck.indexOf(\":\");\n\t\tif (colonIndex != -1) {\n\t\t\tif (debugLogging) {addLogEntry(\"Updating \" ~ pathToCheck ~ \" to remove prefix up to ':'\", [\"debug\"]);}\n\t\t\tpathToCheck = pathToCheck[colonIndex + 1 .. $];\n\t\t\tif (debugLogging) {addLogEntry(\"Updated path: \" ~ pathToCheck, [\"debug\"]);}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// return updated path\n\t\treturn pathToCheck;\n\t}\n\t\n\t// Generate path from JSON data\n\tstring generatePathFromJSONData(JSONValue onedriveJSONItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Function variables\n\t\tstring parentPath;\n\t\tstring combinedPath;\n\t\tstring computedItemPath;\n\t\tbool parentInDatabase = false;\n\t\t\n\t\t// Set itemName\n\t\tstring itemName = onedriveJSONItem[\"name\"].str;\n\t\t// If this item is on our 'driveId' then use the following, otherwise we need to calculate parental path to display the 'correct' path\n\t\tstring thisItemDriveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\tstring thisItemParentId = onedriveJSONItem[\"parentReference\"][\"id\"].str;\n\t\t\n\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\tthisItemDriveId = transformToLowerCase(thisItemDriveId);\n\t\t}\n\t\t\n\t\tif (thisItemDriveId == appConfig.defaultDriveId) {\n\t\t\t// As this is on our driveId, use the path details as is\n\t\t\tparentPath = onedriveJSONItem[\"parentReference\"][\"path\"].str;\n\t\t\tcombinedPath = buildNormalizedPath(buildPath(parentPath, itemName));\n\t\t} else {\n\t\t\t// As this is not our driveId, the 'path' reference above is the 'full' remote path, which is not reflective of our location'\n\t\t\t// Are the 'parent' details in the database?\n\t\t\tparentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId);\n\t\t\tif (parentInDatabase) {\n\t\t\t\t// Parent in DB .. we can calculate path\n\t\t\t\tcomputedItemPath = computeItemPath(thisItemDriveId, thisItemParentId);\n\t\t\t\tcombinedPath = buildNormalizedPath(buildPath(computedItemPath, itemName));\n\t\t\t} else {\n\t\t\t\t// We cant calculate this path\n\t\t\t\tparentPath = onedriveJSONItem[\"parentReference\"][\"name\"].str;\n\t\t\t\tcombinedPath = buildNormalizedPath(buildPath(parentPath, itemName));\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\treturn processPathToRemoveRootReference(combinedPath);\n\t}\n\t\n\t// Function to find a given DriveId in the onlineDriveDetails associative array that maps driveId to DriveDetailsCache\n\t// If 'true' will return 'driveDetails' containing the struct data 'DriveDetailsCache'\n\tbool canFindDriveId(string driveId, out DriveDetailsCache driveDetails) {\n\t\t\n\t\t// Not adding performance metrics to this function\n\t\n\t\tauto ptr = driveId in onlineDriveDetails;\n\t\tif (ptr !is null) {\n\t\t\tdriveDetails = *ptr; // Dereference the pointer to get the value\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\t\n\t// Add this driveId plus relevant details for future reference and use\n\tvoid addOrUpdateOneDriveOnlineDetails(string driveId) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\n\t\tbool quotaRestricted;\n\t\tbool quotaAvailable;\n\t\tlong quotaRemaining;\n\t\t\n\t\t// Get the data from online\n\t\tauto onlineDriveData = getRemainingFreeSpaceOnline(driveId);\n\t\tquotaRestricted = to!bool(onlineDriveData[0][0]);\n\t\tquotaAvailable = to!bool(onlineDriveData[0][1]);\n\t\tquotaRemaining = to!long(onlineDriveData[0][2]);\n\t\tonlineDriveDetails[driveId] = DriveDetailsCache(driveId, quotaRestricted, quotaAvailable, quotaRemaining);\n\t\t\n\t\t// Debug log what the cached array now contains\n\t\tif (debugLogging) {addLogEntry(\"onlineDriveDetails: \" ~ to!string(onlineDriveDetails), [\"debug\"]);}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\n\t// Return a specific 'driveId' details from 'onlineDriveDetails'\n\tDriveDetailsCache getDriveDetails(string driveId) {\n\t\t\n\t\t// Not adding performance metrics to this function\n\t\t\n\t\tauto ptr = driveId in onlineDriveDetails;\n\t\tif (ptr !is null) {\n\t\t\treturn *ptr;  // Dereference the pointer to get the value\n\t\t} else {\n\t\t\t// Return a default DriveDetailsCache or handle the case where the driveId is not found\n\t\t\treturn DriveDetailsCache.init; // Return default-initialised struct\n\t\t}\n\t}\n\t\n\t// Search a given Drive ID, Item ID and filename to see if this exists in the location specified\n\tJSONValue searchDriveItemForFile(string parentItemDriveId, string parentItemId, string fileToUpload) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tJSONValue onedriveJSONItem;\n\t\tstring searchName = baseName(fileToUpload);\n\t\tJSONValue thisLevelChildren;\n\t\tstring nextLink;\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi checkFileOneDriveApiInstance;\n\t\tcheckFileOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tcheckFileOneDriveApiInstance.initialise();\n\t\t\n\t\twhile (true) {\n\t\t\t// Check if exitHandlerTriggered is true\n\t\t\tif (exitHandlerTriggered) {\n\t\t\t\t// break out of the 'while (true)' loop\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\n\t\t\t// Try and query top level children\n\t\t\ttry {\n\t\t\t\tthisLevelChildren = checkFileOneDriveApiInstance.listChildren(parentItemDriveId, parentItemId, nextLink);\n\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t// OneDrive threw an error\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(debugLogBreakType1, [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"Query Error: thisLevelChildren = checkFileOneDriveApiInstance.listChildren(parentItemDriveId, parentItemId, nextLink)\", [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"driveId:   \" ~ parentItemDriveId, [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"idToQuery: \" ~ parentItemId, [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"nextLink:  \" ~ nextLink, [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Handle the 404 error code - the parent item id was not found on the drive id specified\n\t\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t\t// Return an empty JSON item, as parent item could not be found, thus any child object will never be found\n\t\t\t\t\treturn onedriveJSONItem;\n\t\t\t\t} else {\n\t\t\t\t\t// Default operation if not 408,429,503,504 errors\n\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance\n\t\t\t\t\t// Display what the error is\n\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// 'thisLevelChildren' must be a valid JSON response to progress any further\n\t\t\tif (thisLevelChildren.type() == JSONType.object) {\n\t\t\t\t// Process thisLevelChildren response\n\t\t\t\tforeach (child; thisLevelChildren[\"value\"].array) {\n\t\t\t\t\t// Only looking at files\n\t\t\t\t\tif ((child[\"name\"].str == searchName) && ((\"file\" in child) != null)) {\n\t\t\t\t\t\t// Found the matching file, return its JSON representation\n\t\t\t\t\t\t// Operations in this thread are done / complete\n\t\t\t\t\t\t\n\t\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\t\tcheckFileOneDriveApiInstance.releaseCurlEngine();\n\t\t\t\t\t\tcheckFileOneDriveApiInstance = null;\n\t\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\t\tGC.collect();\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Return child as found item\n\t\t\t\t\t\treturn child;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response \n\t\t\t\t// to indicate more items are available and provide the request URL for the next page of items.\n\t\t\t\tif (\"@odata.nextLink\" in thisLevelChildren) {\n\t\t\t\t\t// Update nextLink to next changeSet bundle\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Setting nextLink to (@odata.nextLink): \" ~ nextLink, [\"debug\"]);}\n\t\t\t\t\tnextLink = thisLevelChildren[\"@odata.nextLink\"].str;\n\t\t\t\t} else break;\n\t\t\t\t\n\t\t\t\t// Sleep for a while to avoid busy-waiting\n\t\t\t\tThread.sleep(dur!\"msecs\"(100)); // Adjust the sleep duration as needed\n\t\t\t} else {\n\t\t\t\t// API response was not a valid response\n\t\t\t\t// Break out of the 'while (true)' loop\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tcheckFileOneDriveApiInstance.releaseCurlEngine();\n\t\tcheckFileOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\t\t\t\n\t\t// return an empty JSON item, as search item was not found\n\t\treturn onedriveJSONItem;\n\t}\n\t\n\t// Update 'onlineDriveDetails' with the latest data about this drive\n\tvoid updateDriveDetailsCache(string driveId, bool quotaRestricted, bool quotaAvailable, long localFileSize) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// As each thread is running differently, what is the current 'quotaRemaining' for 'driveId' ?\n\t\tlong quotaRemaining;\n\t\tDriveDetailsCache cachedOnlineDriveData;\n\t\tcachedOnlineDriveData = getDriveDetails(driveId);\n\t\tquotaRemaining = cachedOnlineDriveData.quotaRemaining;\n\t\t\n\t\t// Update 'quotaRemaining'\n\t\tquotaRemaining = quotaRemaining - localFileSize;\n\t\t\n\t\t// Do the flags get updated?\n\t\tif (quotaRemaining <= 0) {\n\t\t\tif (appConfig.accountType == \"personal\"){\n\t\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\t\tdriveId = transformToLowerCase(driveId);\n\t\t\t\n\t\t\t\tif (driveId == appConfig.defaultDriveId) {\n\t\t\t\t\t// zero space available on our drive\n\t\t\t\t\taddLogEntry(\"ERROR: OneDrive account currently has zero space available. Please free up some space online or purchase additional capacity.\");\n\t\t\t\t\tquotaRemaining = 0;\n\t\t\t\t\tquotaAvailable = false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// zero space available is being reported, maybe being restricted?\n\t\t\t\tif (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\"]);}\n\t\t\t\tquotaRemaining = 0;\n\t\t\t\tquotaRestricted = true;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Updated the details\n\t\tonlineDriveDetails[driveId] = DriveDetailsCache(driveId, quotaRestricted, quotaAvailable, quotaRemaining);\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Update all of the known cached driveId quota details\n\tvoid freshenCachedDriveQuotaDetails() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\tforeach (driveId; onlineDriveDetails.keys) {\n\t\t\t// Update this driveid quota details\n\t\t\tif (debugLogging) {addLogEntry(\"Freshen Quota Details for this driveId: \" ~ driveId, [\"debug\"]);}\n\t\t\taddOrUpdateOneDriveOnlineDetails(driveId);\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Create a 'root' DB Tie Record for a Shared Folder from the JSON data\n\tvoid createDatabaseRootTieRecordForOnlineSharedFolder(JSONValue onedriveJSONItem, string relocatedFolderDriveId = null, string relocatedFolderParentId = null) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Creating|Updating a DB Tie\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Creating|Updating a 'root' DB Tie Record for this Shared Folder (Actual 'Shared With Me' Folder Name): \" ~ onedriveJSONItem[\"name\"].str, [\"debug\"]);\n\t\t\taddLogEntry(\"Raw JSON for 'root' DB Tie Record: \" ~ to!string(onedriveJSONItem), [\"debug\"]);\n\t\t}\n\n\t\t// New DB Tie Item to detail the 'root' of the Shared Folder\n\t\tItem tieDBItem;\n\t\tstring lastModifiedTimestamp;\n\t\ttieDBItem.name = \"root\";\n\t\t\n\t\t// Get the right parentReference details\n\t\tif (isItemRemote(onedriveJSONItem)) {\n\t\t\ttieDBItem.driveId = onedriveJSONItem[\"remoteItem\"][\"parentReference\"][\"driveId\"].str;\n\t\t\ttieDBItem.id = onedriveJSONItem[\"remoteItem\"][\"id\"].str;\n\t\t} else {\n\t\t\tif (onedriveJSONItem[\"name\"].str != \"root\") {\n\t\t\t\ttieDBItem.driveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\t\n\t\t\t\t// OneDrive Personal JSON responses are in-consistent with not having 'id' available\n\t\t\t\tif (hasParentReferenceId(onedriveJSONItem)) {\n\t\t\t\t\t// Use the parent reference id\n\t\t\t\t\ttieDBItem.id = onedriveJSONItem[\"parentReference\"][\"id\"].str;\n\t\t\t\t} else {\n\t\t\t\t\t// Testing evidence shows that for Personal accounts, use the 'id' itself\n\t\t\t\t\ttieDBItem.id = onedriveJSONItem[\"id\"].str;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttieDBItem.driveId = onedriveJSONItem[\"parentReference\"][\"driveId\"].str;\n\t\t\t\ttieDBItem.id = onedriveJSONItem[\"id\"].str;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// set the item type\n\t\ttieDBItem.type = ItemType.root;\n\t\t\n\t\t// get the lastModifiedDateTime\n\t\tlastModifiedTimestamp = strip(onedriveJSONItem[\"fileSystemInfo\"][\"lastModifiedDateTime\"].str);\n\t\t// is lastModifiedTimestamp valid?\n\t\tif (isValidUTCDateTime(lastModifiedTimestamp)) {\n\t\t\t// string is a valid timestamp\n\t\t\ttieDBItem.mtime = SysTime.fromISOExtString(lastModifiedTimestamp);\n\t\t} else {\n\t\t\t// invalid timestamp from JSON file\n\t\t\taddLogEntry(\"WARNING: Invalid timestamp provided by the Microsoft OneDrive API: \" ~ lastModifiedTimestamp);\n\t\t\t// Set mtime to SysTime(0)\n\t\t\ttieDBItem.mtime = SysTime(0);\n\t\t}\n\t\t\n\t\t// Ensure there is no parentId for this DB record\n\t\ttieDBItem.parentId = null;\n\t\t\n\t\t// OneDrive Personal and Business supports relocating Shared Folders to other folders.\n\t\t// This means, in our DB, we need this DB record to have the correct parentId of the parental folder, if this is relocated shared folder\n\t\t// This is stored in the 'relocParentId' DB entry\n\t\t// This 'relocatedFolderParentId' variable is only ever set if using OneDrive Business account types and the shared folder is located online in another folder\n\t\tif ((!relocatedFolderDriveId.empty) && (!relocatedFolderParentId.empty)) {\n\t\t\t// Ensure that we set the relocParentId to the provided relocatedFolderParentId record\n\t\t\tif (debugLogging) {addLogEntry(\"Relocated Shared Folder references were provided - adding these to the 'root' DB Tie Record\", [\"debug\"]);}\n\t\t\ttieDBItem.relocDriveId = relocatedFolderDriveId;\n\t\t\ttieDBItem.relocParentId = relocatedFolderParentId;\n\t\t}\n\t\t\n\t\t// Issue #3115 - Validate driveId length\n\t\t// What account type is this?\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Issue #3336 - Convert driveId to lowercase before any test\n\t\t\ttieDBItem.driveId = transformToLowerCase(tieDBItem.driveId);\n\t\t\t\n\t\t\t// Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId\n\t\t\tif (tieDBItem.driveId != appConfig.defaultDriveId) {\n\t\t\t\ttieDBItem.driveId = testProvidedDriveIdForLengthIssue(tieDBItem.driveId);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Add this DB Tie parent record to the local database\n\t\tif (debugLogging) {addLogEntry(\"Creating|Updating into local database a 'root' DB Tie record for a OneDrive Shared Folder online: \" ~ to!string(tieDBItem), [\"debug\"]);}\n\t\titemDB.upsert(tieDBItem);\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Create a DB Tie Record for a Shared Folder \n\tvoid createDatabaseTieRecordForOnlineSharedFolder(Item parentItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Creating|Updating a DB Tie\n\t\tif (debugLogging) {\n\t\t\t//addLogEntry(\"Creating|Updating a DB Tie Record for this Shared Folder: \" ~ parentItem.name, [\"debug\"]);\n\t\t\taddLogEntry(\"Creating|Updating a DB Tie Record for this Shared Folder from the provided parental data: \" ~ parentItem.name, [\"debug\"]);\n\t\t\taddLogEntry(\"Parent Item Record: \" ~ to!string(parentItem), [\"debug\"]);\n\t\t}\n\t\t\n\t\t// New DB Tie Item to bind the 'remote' path to our parent path in the database\n\t\tItem tieDBItem;\n\t\ttieDBItem.name = parentItem.name;\n\t\ttieDBItem.id = parentItem.remoteId;\n\t\ttieDBItem.type = ItemType.dir;\n\t\ttieDBItem.mtime = parentItem.mtime;\n\t\t\n\t\t// Initially set this\n\t\ttieDBItem.driveId = parentItem.remoteDriveId;\n\t\t\n\t\t// What account type is this as this determines what 'tieDBItem.parentId' should be set to\n\t\t// There is a difference in the JSON responses between 'personal' and 'business' account types for Shared Folders\n\t\t// Essentially an API inconsistency\n\t\tif (appConfig.accountType == \"personal\") {\n\t\t\t// Set tieDBItem.parentId to null\n\t\t\ttieDBItem.parentId = null;\n\t\t\ttieDBItem.type = ItemType.root;\n\t\t\t\n\t\t\t// Issue #3136, #3139 #3143\n\t\t\t// Fetch the actual online record for this item\n\t\t\t// This returns the actual OneDrive Personal driveId value and is 15 character checked\n\t\t\tstring actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(tieDBItem.driveId));\n\t\t\ttieDBItem.driveId = actualOnlineDriveId;\n\t\t} else {\n\t\t\t// The tieDBItem.parentId needs to be the correct driveId id reference\n\t\t\t// Query the DB \n\t\t\tItem[] rootDriveItems;\n\t\t\tItem dbRecord;\n\t\t\trootDriveItems = itemDB.selectByDriveId(parentItem.remoteDriveId);\n\t\t\t\n\t\t\t// Fix Issue #2883\n\t\t\tif (rootDriveItems.length > 0) {\n\t\t\t\t// Use the first record returned\n\t\t\t\tdbRecord = rootDriveItems[0];\n\t\t\t\ttieDBItem.parentId = dbRecord.id;\n\t\t\t} else {\n\t\t\t\t// 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\n\t\t\t\tif (debugLogging) {addLogEntry(\"itemDB.selectByDriveId(parentItem.remoteDriveId) returned zero database entries for this remoteDriveId: \" ~ to!string(parentItem.remoteDriveId), [\"debug\"]);}\n\t\t\t\n\t\t\t\t// Create a new API Instance for this query and initialise it\n\t\t\t\tOneDriveApi getPathDetailsApiInstance;\n\t\t\t\tJSONValue latestOnlineDetails;\n\t\t\t\tgetPathDetailsApiInstance = new OneDriveApi(appConfig);\n\t\t\t\tgetPathDetailsApiInstance.initialise();\n\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\t// Get the latest online details\n\t\t\t\t\tlatestOnlineDetails = getPathDetailsApiInstance.getPathDetailsById(parentItem.remoteDriveId, parentItem.remoteId);\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Parent JSON details from Online Query: \" ~ to!string(latestOnlineDetails), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Convert JSON to a database compatible item\n\t\t\t\t\tItem tempOnlineRecord = makeItem(latestOnlineDetails);\n\t\t\t\t\t\n\t\t\t\t\t// Configure tieDBItem.parentId to use tempOnlineRecord.id\n\t\t\t\t\ttieDBItem.parentId = tempOnlineRecord.id;\n\t\t\t\n\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\tgetPathDetailsApiInstance.releaseCurlEngine();\n\t\t\t\t\tgetPathDetailsApiInstance = null;\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\n\t\t\t\t} catch (OneDriveException e) {\n\t\t\t\t\t// Display error message\n\t\t\t\t\tdisplayOneDriveErrorMessage(e.msg, thisFunctionName);\n\t\t\t\t\t\n\t\t\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\t\t\tgetPathDetailsApiInstance.releaseCurlEngine();\n\t\t\t\t\tgetPathDetailsApiInstance = null;\n\t\t\t\t\t// Perform Garbage Collection\n\t\t\t\t\tGC.collect();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Free the array memory\n\t\t\trootDriveItems = [];\n\t\t}\n\t\t\n\t\t// Add tie DB record to the local database\n\t\tif (debugLogging) {addLogEntry(\"Creating|Updating into local database a DB Tie record: \" ~ to!string(tieDBItem), [\"debug\"]);}\n\t\titemDB.upsert(tieDBItem);\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// List all the OneDrive Business Shared Items for the user to see\n\tvoid listBusinessSharedObjects() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tJSONValue sharedWithMeItems;\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi sharedWithMeOneDriveApiInstance;\n\t\tsharedWithMeOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tsharedWithMeOneDriveApiInstance.initialise();\n\t\t\n\t\ttry {\n\t\t\tsharedWithMeItems = sharedWithMeOneDriveApiInstance.getSharedWithMe();\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tsharedWithMeOneDriveApiInstance.releaseCurlEngine();\n\t\t\tsharedWithMeOneDriveApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t} catch (OneDriveException e) {\n\t\t\t// Display error message\n\t\t\tdisplayOneDriveErrorMessage(e.msg, thisFunctionName);\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tsharedWithMeOneDriveApiInstance.releaseCurlEngine();\n\t\t\tsharedWithMeOneDriveApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tif (sharedWithMeItems.type() == JSONType.object) {\n\t\t\n\t\t\tif (count(sharedWithMeItems[\"value\"].array) > 0) {\n\t\t\t\t// No shared items\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"Listing available OneDrive Business Shared Items:\");\n\t\t\t\taddLogEntry();\n\t\t\t\t\n\t\t\t\t// Iterate through the array\n\t\t\t\tforeach (searchResult; sharedWithMeItems[\"value\"].array) {\n\t\t\t\t\n\t\t\t\t\t// loop variables for each item\n\t\t\t\t\tstring sharedByName;\n\t\t\t\t\tstring sharedByEmail;\n\t\t\t\t\t\n\t\t\t\t\t// Debug response output\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"shared folder entry: \" ~ to!string(searchResult), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Configure 'who' this was shared by\n\t\t\t\t\tif (\"sharedBy\" in searchResult[\"remoteItem\"][\"shared\"]) {\n\t\t\t\t\t\t// we have shared by details we can use\n\t\t\t\t\t\tif (\"displayName\" in searchResult[\"remoteItem\"][\"shared\"][\"sharedBy\"][\"user\"]) {\n\t\t\t\t\t\t\tsharedByName = searchResult[\"remoteItem\"][\"shared\"][\"sharedBy\"][\"user\"][\"displayName\"].str;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (\"email\" in searchResult[\"remoteItem\"][\"shared\"][\"sharedBy\"][\"user\"]) {\n\t\t\t\t\t\t\tsharedByEmail = searchResult[\"remoteItem\"][\"shared\"][\"sharedBy\"][\"user\"][\"email\"].str;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Output query result\n\t\t\t\t\taddLogEntry(debugLogBreakType1);\n\t\t\t\t\tif (isItemFile(searchResult)) {\n\t\t\t\t\t\taddLogEntry(\"Shared File:     \" ~ to!string(searchResult[\"name\"].str));\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddLogEntry(\"Shared Folder:   \" ~ to!string(searchResult[\"name\"].str));\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Detail 'who' shared this\n\t\t\t\t\tif ((sharedByName != \"\") && (sharedByEmail != \"\")) {\n\t\t\t\t\t\taddLogEntry(\"Shared By:       \" ~ sharedByName ~ \" (\" ~ sharedByEmail ~ \")\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (sharedByName != \"\") {\n\t\t\t\t\t\t\taddLogEntry(\"Shared By:       \" ~ sharedByName);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// More detail if --verbose is being used\n\t\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t\taddLogEntry(\"Item Id:         \" ~ searchResult[\"remoteItem\"][\"id\"].str, [\"verbose\"]);\n\t\t\t\t\t\taddLogEntry(\"Parent Drive Id: \" ~ searchResult[\"remoteItem\"][\"parentReference\"][\"driveId\"].str, [\"verbose\"]);\n\t\t\t\t\t\tif (\"id\" in searchResult[\"remoteItem\"][\"parentReference\"]) {\n\t\t\t\t\t\t\taddLogEntry(\"Parent Item Id:  \" ~ searchResult[\"remoteItem\"][\"parentReference\"][\"id\"].str, [\"verbose\"]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Close out the loop\n\t\t\t\taddLogEntry(debugLogBreakType1);\n\t\t\t\taddLogEntry();\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t// No shared items\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"No OneDrive Business Shared Folders were returned\");\n\t\t\t\taddLogEntry();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Query all the OneDrive Business Shared Objects to sync only Shared Files\n\tvoid queryBusinessSharedObjects() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tJSONValue sharedWithMeItems;\n\t\tItem sharedFilesRootDirectoryDatabaseRecord;\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi sharedWithMeOneDriveApiInstance;\n\t\tsharedWithMeOneDriveApiInstance = new OneDriveApi(appConfig);\n\t\tsharedWithMeOneDriveApiInstance.initialise();\n\t\t\n\t\ttry {\n\t\t\tsharedWithMeItems = sharedWithMeOneDriveApiInstance.getSharedWithMe();\n\t\t\t\n\t\t\t// We cant shutdown the API instance here, as we reuse it below\n\t\t\t\n\t\t} catch (OneDriveException e) {\n\t\t\t// Display error message\n\t\t\tdisplayOneDriveErrorMessage(e.msg, thisFunctionName);\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tsharedWithMeOneDriveApiInstance.releaseCurlEngine();\n\t\t\tsharedWithMeOneDriveApiInstance = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Valid JSON response\n\t\tif (sharedWithMeItems.type() == JSONType.object) {\n\t\t\n\t\t\t// Get the configuredBusinessSharedFilesDirectoryName DB item\n\t\t\t// We need this as we need to 'fake' create all the folders for the shared files\n\t\t\t// Then fake create the file entries for the database with the correct parent folder that is the local folder\n\t\t\titemDB.selectByPath(baseName(appConfig.configuredBusinessSharedFilesDirectoryName), appConfig.defaultDriveId, sharedFilesRootDirectoryDatabaseRecord);\n\t\t\n\t\t\t// For each item returned, if a file, process it\n\t\t\tforeach (searchResult; sharedWithMeItems[\"value\"].array) {\n\t\t\t\n\t\t\t\t// Shared Business Folders are added to the account using 'Add shortcut to My files'\n\t\t\t\t// We only care here about any remaining 'files' that are shared with the user\n\t\t\t\t\n\t\t\t\tif (isItemFile(searchResult)) {\n\t\t\t\t\t// Debug response output\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"getSharedWithMe Response Shared File JSON: \" ~ sanitiseJSONItem(searchResult), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Make a DB item from this JSON\n\t\t\t\t\tItem sharedFileOriginalData = makeItem(searchResult);\n\t\t\t\t\t\n\t\t\t\t\t// Variables for each item\n\t\t\t\t\tstring sharedByName;\n\t\t\t\t\tstring sharedByEmail;\n\t\t\t\t\tstring sharedByFolderName;\n\t\t\t\t\tstring newLocalSharedFilePath;\n\t\t\t\t\tstring newItemPath;\n\t\t\t\t\tItem sharedFilesPath;\n\t\t\t\t\tJSONValue fileToDownload;\n\t\t\t\t\tJSONValue detailsToUpdate;\n\t\t\t\t\tJSONValue latestOnlineDetails;\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t// Configure 'who' this was shared by\n\t\t\t\t\tif (\"sharedBy\" in searchResult[\"remoteItem\"][\"shared\"]) {\n\t\t\t\t\t\t// we have shared by details we can use\n\t\t\t\t\t\tif (\"displayName\" in searchResult[\"remoteItem\"][\"shared\"][\"sharedBy\"][\"user\"]) {\n\t\t\t\t\t\t\tsharedByName = searchResult[\"remoteItem\"][\"shared\"][\"sharedBy\"][\"user\"][\"displayName\"].str;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (\"email\" in searchResult[\"remoteItem\"][\"shared\"][\"sharedBy\"][\"user\"]) {\n\t\t\t\t\t\t\tsharedByEmail = searchResult[\"remoteItem\"][\"shared\"][\"sharedBy\"][\"user\"][\"email\"].str;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Configure 'who' shared this, so that we can create the directory for that users shared files with us\n\t\t\t\t\tif ((sharedByName != \"\") && (sharedByEmail != \"\")) {\n\t\t\t\t\t\tsharedByFolderName = sharedByName ~ \" (\" ~ sharedByEmail ~ \")\";\n\t\t\t\t\t\t\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Either name or email is not defined -> check specifically. Currently: \" ~ to!string(sharedByName) ~ \" / \" ~ to!string(sharedByEmail), [\"debug\"]);}\n\t\t\t\t\t\tif (sharedByName != \"\") {\n\t\t\t\t\t\t\tsharedByFolderName = sharedByName;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsharedByFolderName = sharedByEmail;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Combined folder set to \" ~ to!string(sharedByFolderName), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Create the local path to store this users shared files with us\n\t\t\t\t\tnewLocalSharedFilePath = buildNormalizedPath(buildPath(appConfig.configuredBusinessSharedFilesDirectoryName, sharedByFolderName));\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"newLocalSharedFilePath is located at \" ~ to!string(newLocalSharedFilePath), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Does the Shared File Users Local Directory to store the shared file(s) exist?\n\t\t\t\t\tif (!exists(newLocalSharedFilePath)) {\n\t\t\t\t\t\t// Folder does not exist locally and needs to be created\n\t\t\t\t\t\taddLogEntry(\"Creating the OneDrive Business Shared File Users Local Directory: \" ~ newLocalSharedFilePath);\n\t\t\t\t\t\n\t\t\t\t\t\tif (!dryRun) {\n\t\t\t\t\t\t\t// Local folder does not exist, thus needs to be created\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t// Attempt path creation\n\t\t\t\t\t\t\t\tmkdirRecurse(newLocalSharedFilePath);\n\t\t\t\t\t\t\t} catch (std.file.FileException e) {\n\t\t\t\t\t\t\t\t// Creating the path failed\n\t\t\t\t\t\t\t\taddLogEntry(\"ERROR: Unable to create the OneDrive Business Shared File Users Local Directory: \" ~ e.msg, [\"info\", \"notify\"]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t\n\t\t\t\t\t\t// As this will not be created online, generate a response so it can be saved to the database\n\t\t\t\t\t\tsharedFilesPath = makeItem(createFakeResponse(baseName(newLocalSharedFilePath)));\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Update sharedFilesPath parent items to that of sharedFilesRootDirectoryDatabaseRecord\n\t\t\t\t\t\tsharedFilesPath.parentId = sharedFilesRootDirectoryDatabaseRecord.id;\n\t\t\t\t\t\t\n\t\t\t\t\t\t// Add DB record to the local database\n\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Creating|Updating into local database a DB record for storing OneDrive Business Shared Files: \" ~ to!string(sharedFilesPath), [\"debug\"]);}\n\t\t\t\t\t\titemDB.upsert(sharedFilesPath);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Folder exists locally, is the folder in the database? \n\t\t\t\t\t\t// Query DB for this path\n\t\t\t\t\t\tItem dbRecord;\n\t\t\t\t\t\tif (!itemDB.selectByPath(baseName(newLocalSharedFilePath), appConfig.defaultDriveId, dbRecord)) {\n\t\t\t\t\t\t\t// As this will not be created online, generate a response so it can be saved to the database\n\t\t\t\t\t\t\tsharedFilesPath = makeItem(createFakeResponse(baseName(newLocalSharedFilePath)));\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Update sharedFilesPath parent items to that of sharedFilesRootDirectoryDatabaseRecord\n\t\t\t\t\t\t\tsharedFilesPath.parentId = sharedFilesRootDirectoryDatabaseRecord.id;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Add DB record to the local database\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Creating|Updating into local database a DB record for storing OneDrive Business Shared Files: \" ~ to!string(sharedFilesPath), [\"debug\"]);}\n\t\t\t\t\t\t\titemDB.upsert(sharedFilesPath);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// If the folder exists in the db, assign the variable to have the parentID available\n\t\t\t\t\t\t\tsharedFilesPath = dbRecord;\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Recreating local database record for storing OneDrive Business Shared Files: \" ~ to!string(sharedFilesPath), [\"debug\"]);}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// The file to download JSON details\n\t\t\t\t\tfileToDownload = searchResult;\n\t\t\t\t\t\n\t\t\t\t\t// Get the latest online details\n\t\t\t\t\tlatestOnlineDetails = sharedWithMeOneDriveApiInstance.getPathDetailsById(sharedFileOriginalData.remoteDriveId, sharedFileOriginalData.remoteId);\n\t\t\t\t\tItem tempOnlineRecord = makeItem(latestOnlineDetails);\n\t\t\t\t\t\n\t\t\t\t\t// With the local folders created, now update 'fileToDownload' to download the file to our location:\n\t\t\t\t\t//\t\"parentReference\": {\n\t\t\t\t\t//\t\t\"driveId\": \"<account drive id>\",\n\t\t\t\t\t//\t\t\"driveType\": \"business\",\n\t\t\t\t\t//\t\t\"id\": \"<local users shared folder id>\",\n\t\t\t\t\t//\t},\n\t\t\t\t\t\n\t\t\t\t\t// The getSharedWithMe() JSON response also contains an API bug where the 'hash' of the file is not provided\n\t\t\t\t\t// Use the 'latestOnlineDetails' response to obtain the hash\n\t\t\t\t\t//\t\"file\": {\n\t\t\t\t\t//\t\t\"hashes\": {\n\t\t\t\t\t//\t\t\t\"quickXorHash\": \"<hash value>\"\n\t\t\t\t\t//\t\t}\n\t\t\t\t\t//\t},\n\t\t\t\t\t//\n\t\t\t\t\t\n\t\t\t\t\t// The getSharedWithMe() JSON response also contains an API bug where the 'size' of the file is not the actual size of the file\n\t\t\t\t\t// The getSharedWithMe() JSON response also contains an API bug where the 'eTag' of the file is not present\n\t\t\t\t\t// 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\n\t\t\t\t\t\n\t\t\t\t\tdetailsToUpdate = [\n\t\t\t\t\t\t\t\t\"parentReference\": JSONValue([\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"driveId\": JSONValue(appConfig.defaultDriveId),\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"driveType\": JSONValue(\"business\"),\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"id\": JSONValue(sharedFilesPath.id)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\t\"file\": JSONValue([\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"hashes\":JSONValue([\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"quickXorHash\": JSONValue(tempOnlineRecord.quickXorHash)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\t\"eTag\": JSONValue(tempOnlineRecord.eTag)\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\n\t\t\t\t\tforeach (string key, JSONValue value; detailsToUpdate.object) {\n\t\t\t\t\t\tfileToDownload[key] = value;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Update specific items\n\t\t\t\t\t// Update 'size'\n\t\t\t\t\tfileToDownload[\"size\"] = to!int(tempOnlineRecord.size);\n\t\t\t\t\tfileToDownload[\"remoteItem\"][\"size\"] = to!int(tempOnlineRecord.size);\n\t\t\t\t\t// Update 'lastModifiedDateTime'\n\t\t\t\t\tfileToDownload[\"lastModifiedDateTime\"] = latestOnlineDetails[\"fileSystemInfo\"][\"lastModifiedDateTime\"].str;\n\t\t\t\t\tfileToDownload[\"fileSystemInfo\"][\"lastModifiedDateTime\"] = latestOnlineDetails[\"fileSystemInfo\"][\"lastModifiedDateTime\"].str;\n\t\t\t\t\tfileToDownload[\"remoteItem\"][\"lastModifiedDateTime\"] = latestOnlineDetails[\"fileSystemInfo\"][\"lastModifiedDateTime\"].str;\n\t\t\t\t\tfileToDownload[\"remoteItem\"][\"fileSystemInfo\"][\"lastModifiedDateTime\"] = latestOnlineDetails[\"fileSystemInfo\"][\"lastModifiedDateTime\"].str;\n\t\t\t\t\t\n\t\t\t\t\t// Final JSON that will be used to download the file\n\t\t\t\t\tif (debugLogging) {addLogEntry(\"Final fileToDownload: \" ~ to!string(fileToDownload), [\"debug\"]);}\n\t\t\t\t\t\n\t\t\t\t\t// Make the new DB item from the consolidated JSON item\n\t\t\t\t\tItem downloadSharedFileDbItem = makeItem(fileToDownload);\n\t\t\t\t\t\n\t\t\t\t\t// Calculate the full local path for this shared file\n\t\t\t\t\tnewItemPath = computeItemPath(downloadSharedFileDbItem.driveId, downloadSharedFileDbItem.parentId) ~ \"/\" ~ downloadSharedFileDbItem.name;\n\t\t\t\t\t\n\t\t\t\t\t// Does this potential file exists on disk?\n\t\t\t\t\tif (!exists(newItemPath)) {\n\t\t\t\t\t\t// The shared file does not exists locally\n\t\t\t\t\t\t// Is this something we actually want? Check the JSON against Client Side Filtering Rules\n\t\t\t\t\t\tbool unwanted = checkJSONAgainstClientSideFiltering(fileToDownload);\n\t\t\t\t\t\tif (!unwanted) {\n\t\t\t\t\t\t\t// File has not been excluded via Client Side Filtering\n\t\t\t\t\t\t\t// Submit this shared file to be processed further for downloading\n\t\t\t\t\t\t\tapplyPotentiallyNewLocalItem(downloadSharedFileDbItem, fileToDownload, newItemPath);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// A file, in the desired local location already exists with the same name\n\t\t\t\t\t\t// Is this local file in sync?\n\t\t\t\t\t\tstring itemSource = \"remote\";\n\t\t\t\t\t\tif (!isItemSynced(downloadSharedFileDbItem, newItemPath, itemSource)) {\n\t\t\t\t\t\t\t// Not in sync ....\n\t\t\t\t\t\t\tItem existingDatabaseItem;\n\t\t\t\t\t\t\tbool existingDBEntry = itemDB.selectById(downloadSharedFileDbItem.driveId, downloadSharedFileDbItem.id, existingDatabaseItem);\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// Is there a DB entry?\n\t\t\t\t\t\t\tif (existingDBEntry) {\n\t\t\t\t\t\t\t\t// Existing DB entry\n\t\t\t\t\t\t\t\t// Need to be consistent here with how 'newItemPath' was calculated\n\t\t\t\t\t\t\t\tstring existingItemPath = computeItemPath(existingDatabaseItem.driveId, existingDatabaseItem.parentId) ~ \"/\" ~ existingDatabaseItem.name;\n\t\t\t\t\t\t\t\t// Attempt to apply this changed item\n\t\t\t\t\t\t\t\tapplyPotentiallyChangedItem(existingDatabaseItem, existingItemPath, downloadSharedFileDbItem, newItemPath, fileToDownload);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// File exists locally, it is not in sync, there is no record in the DB of this file\n\t\t\t\t\t\t\t\t// In case the renamed path is needed\n\t\t\t\t\t\t\t\tstring renamedPath;\n\t\t\t\t\t\t\t\t// If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not\n\t\t\t\t\t\t\t\tsafeBackup(newItemPath, dryRun, bypassDataPreservation, renamedPath);\n\t\t\t\t\t\t\t\t// Submit this shared file to be processed further for downloading\n\t\t\t\t\t\t\t\tapplyPotentiallyNewLocalItem(downloadSharedFileDbItem, fileToDownload, newItemPath);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Item is in sync, ensure the DB record is the same\n\t\t\t\t\t\t\titemDB.upsert(downloadSharedFileDbItem);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tsharedWithMeOneDriveApiInstance.releaseCurlEngine();\n\t\tsharedWithMeOneDriveApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Renaming or moving a directory online manually using --source-directory 'path/as/source/' --destination-directory 'path/as/destination'\n\tvoid moveOrRenameDirectoryOnline(string sourcePath, string destinationPath) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Function Variables\n\t\tbool sourcePathExists = false;\n\t\tbool destinationPathExists = false;\n\t\tbool invalidDestination = false;\n\t\tJSONValue sourcePathData;\n\t\tJSONValue destinationPathData;\n\t\tJSONValue parentPathData;\n\t\tItem sourceItem;\n\t\tItem parentItem;\n\t\t\n\t\t// Log that we are doing a move\n\t\taddLogEntry(\"Moving \" ~ sourcePath ~ \" to \" ~ destinationPath);\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi onlineMoveApiInstance;\n\t\tonlineMoveApiInstance = new OneDriveApi(appConfig);\n\t\tonlineMoveApiInstance.initialise();\n\t\t\n\t\t// In order to move, the 'source' needs to exist online, so this is the first check\n\t\ttry {\n\t\t\tsourcePathData = onlineMoveApiInstance.getPathDetails(sourcePath);\n\t\t\tsourceItem = makeItem(sourcePathData);\n\t\t\tsourcePathExists = true;\n\t\t} catch (OneDriveException exception) {\n\t\t\n\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t// The item to search was not found. If it does not exist, how can we move it?\n\t\t\t\taddLogEntry(\"The source path to move does not exist online - unable to move|rename a path that does not already exist online\");\n\t\t\t\tforceExit();\n\t\t\t} else {\n\t\t\t\t// An error, regardless of what it is ... not good\n\t\t\t\t// Display what the error is\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\tforceExit();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// The second check needs to be that the destination does not already exist\n\t\ttry {\n\t\t\tdestinationPathData = onlineMoveApiInstance.getPathDetails(destinationPath);\n\t\t\tdestinationPathExists = true;\n\t\t\taddLogEntry(\"The destination path to move to exists online - unable to move|rename to a path that already exists online\");\n\t\t\tforceExit();\n\t\t} catch (OneDriveException exception) {\n\t\t\n\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t// The item to search was not found. This is good as the destination path is empty\n\t\t\t} else {\n\t\t\t\t// An error, regardless of what it is ... not good\n\t\t\t\t// Display what the error is\n\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\tforceExit();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Can we move?\n\t\tif ((sourcePathExists) && (!destinationPathExists)) {\n\t\t\t// Make an item we can use\n\t\t\tItem onlineItem = makeItem(sourcePathData);\n\t\t\n\t\t\t// The directory to move MUST be a directory\n\t\t\tif (onlineItem.type == ItemType.dir) {\n\t\t\t\n\t\t\t\t// Validate that the 'destination' is valid\n\t\t\t\t\n\t\t\t\t// This not a Client Side Filtering check, nor a Microsoft Check, but is a sanity check that the path provided is UTF encoded correctly\n\t\t\t\t// Check the std.encoding of the path against: Unicode 5.0, ASCII, ISO-8859-1, ISO-8859-2, WINDOWS-1250, WINDOWS-1251, WINDOWS-1252\n\t\t\t\tif (!invalidDestination) {\n\t\t\t\t\tif(!isValid(destinationPath)) {\n\t\t\t\t\t\t// Path is not valid according to https://dlang.org/phobos/std_encoding.html\n\t\t\t\t\t\taddLogEntry(\"Skipping move - invalid character encoding sequence: \" ~ destinationPath, [\"info\", \"notify\"]);\n\t\t\t\t\t\tinvalidDestination = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// We do not check this path against the Client Side Filtering Rules as this is 100% an online move only\n\t\t\t\t\n\t\t\t\t// Check this path against the Microsoft Naming Conventions & Restrictions\n\t\t\t\t// - Check path against Microsoft OneDrive restriction and limitations about Windows naming for files and folders\n\t\t\t\t// - Check path for bad whitespace items\n\t\t\t\t// - Check path for HTML ASCII Codes\n\t\t\t\t// - Check path for ASCII Control Codes\n\t\t\t\tif (!invalidDestination) {\n\t\t\t\t\tinvalidDestination = checkPathAgainstMicrosoftNamingRestrictions(destinationPath, \"move\");\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Is the destination location invalid?\n\t\t\t\tif (!invalidDestination) {\n\t\t\t\t\t// We can perform the online move\n\t\t\t\t\t// We need to query for the parent information of the destination path\n\t\t\t\t\tstring parentPath = dirName(destinationPath);\n\t\t\t\t\t\n\t\t\t\t\t// Configure the parentItem by if this is the account 'root' use the root details, or query online for the parent details\n\t\t\t\t\tif (parentPath == \".\") {\n\t\t\t\t\t\t// Parent path is '.' which is the account root - use client defaults\n\t\t\t\t\t\tparentItem.driveId = appConfig.defaultDriveId; \t// Should give something like 12345abcde1234a1\n\t\t\t\t\t\tparentItem.id = appConfig.defaultRootId;  \t\t// Should give something like 12345ABCDE1234A1!101\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Need to query to obtain the details\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Attempting to query OneDrive Online for this parent path: \" ~ parentPath, [\"debug\"]);}\n\t\t\t\t\t\t\tparentPathData = onlineMoveApiInstance.getPathDetails(parentPath);\n\t\t\t\t\t\t\tif (debugLogging) {addLogEntry(\"Online Parent Path Query Response: \" ~ to!string(parentPathData), [\"debug\"]);}\n\t\t\t\t\t\t\tparentItem = makeItem(parentPathData);\n\t\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t\t\t\t\t// The item to search was not found. If it does not exist, how can we move it?\n\t\t\t\t\t\t\t\taddLogEntry(\"The parent path to move to does not exist online - unable to move|rename a path to a parent that does exist online\");\n\t\t\t\t\t\t\t\tforceExit();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\t\t\tforceExit();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Configure the modification JSON item\n\t\t\t\t\tSysTime mtime;\n\t\t\t\t\t// Use the current system time\n\t\t\t\t\tmtime = Clock.currTime().toUTC();\n\t\t\t\t\t\n\t\t\t\t\tJSONValue data = [\n\t\t\t\t\t\t\"name\": JSONValue(baseName(destinationPath)),\n\t\t\t\t\t\t\"parentReference\": JSONValue([\n\t\t\t\t\t\t\t\"id\": parentItem.id\n\t\t\t\t\t\t]),\n\t\t\t\t\t\t\"fileSystemInfo\": JSONValue([\n\t\t\t\t\t\t\t\"lastModifiedDateTime\": mtime.toISOExtString()\n\t\t\t\t\t\t])\n\t\t\t\t\t];\n\t\t\t\t\t\n\t\t\t\t\t// Try the online move\n\t\t\t\t\ttry {\n\t\t\t\t\t\tonlineMoveApiInstance.updateById(sourceItem.driveId, sourceItem.id, data, sourceItem.eTag);\n\t\t\t\t\t\t// Log that it was successful\n\t\t\t\t\t\taddLogEntry(\"Successfully moved \" ~ sourcePath ~ \" to \" ~ destinationPath);\n\t\t\t\t\t} catch (OneDriveException exception) {\n\t\t\t\t\t\t// Display what the error is\n\t\t\t\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t\t\t\tforceExit();\t\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// The source item is not a directory\n\t\t\t\taddLogEntry(\"ERROR: The source path to move is not a directory\");\n\t\t\t\tforceExit();\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Return an array of the notification parameters when this is called. This implements FR #2760\n\tstring[] fileTransferNotifications() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\t// Based on the configuration option, send the file transfer actions to the GUI notifications if configured\n\t\t// GUI notifications are already sent for files that meet this criteria:\n\t\t// - Skipping a particular item due to an invalid name\n\t\t// - Skipping a particular item due to an invalid symbolic link\n\t\t// - Skipping a particular item due to an invalid UTF sequence\n\t\t// - Skipping a particular item due to an invalid character encoding sequence\n\t\t// - Files that fail to upload\n\t\t// - Files that fail to download\n\t\t//\n\t\t// This is about notifying on:\n\t\t// - Successful file download\n\t\t// - Successful file upload\n\t\t// - Successful deletion locally\n\t\t// - Successful deletion online\n\t\t\n\t\tstring[] loggingOptions;\n\t\t\n\t\tif (appConfig.getValueBool(\"notify_file_actions\")) {\n\t\t\t// Add the 'notify' to enable GUI notifications\n\t\t\tloggingOptions = [\"info\", \"notify\"];\n\t\t} else {\n\t\t\t// Logging to console and/or logfile only\n\t\t\tloggingOptions = [\"info\"];\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\n\t\treturn loggingOptions;\n\t}\n\t\n\t// OneDrive Personal driveId or parentReference driveId must be 16 characters in length\n\tstring testProvidedDriveIdForLengthIssue(string objectParentDriveId) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Due to this function, we need to keep the return <string value>; code, so that this function operates as efficiently as possible.\n\t\t// Whilst this means some extra code / duplication in this function, it cannot be helped\n\t\n\t\t// OneDrive Personal Account driveId and remoteDriveId length check\n\t\t// 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')\n\t\t// - driveId\n\t\t// - remoteDriveId\n\t\t// \n\t\t// Example:\n\t\t//   024470056F5C3E43 (driveId)\n\t\t//   24470056f5c3e43  (remoteDriveId)\n\t\t//\n\t\t// If this is a OneDrive Personal Account, ensure this value is 16 characters, padded by leading zero's if eventually required\n\t\t\n\t\tstring oldEntry;\n\t\tstring newEntry;\n\t\t\n\t\t// Check the provided objectParentDriveId\n\t\tif (!objectParentDriveId.empty) {\n\t\t\t// Ensure objectParentDriveId is 16 characters long by padding with leading zeros if required\n\t\t\tif (debugLogging) {\n\t\t\t\tstring validationMessage = format(\"Validating that the provided OneDrive Personal 'driveId' value '%s' is 16 characters\", objectParentDriveId);\n\t\t\t\taddLogEntry(validationMessage, [\"debug\"]);\n\t\t\t}\n\t\t\t\n\t\t\t// Is this less than 16 characters\n\t\t\tif (objectParentDriveId.length < 16) {\n\t\t\t\t// Debug logging\n\t\t\t\tif (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\"]);}\n\t\t\t\t\n\t\t\t\t// Generate the change\n\t\t\t\toldEntry = objectParentDriveId;\n\t\t\t\tstring onlineDriveValue;\n\t\t\t\t\n\t\t\t\t// Fetch the actual online record for this item\n\t\t\t\t// This returns the actual OneDrive Personal driveId value based on the input value.\n\t\t\t\t// The function 'fetchRealOnlineDriveIdentifier' does not check for length issue, this is done below\n\t\t\t\tonlineDriveValue = fetchRealOnlineDriveIdentifier(oldEntry);\n\t\t\t\t\n\t\t\t\t// Check the onlineDriveValue value for 15 character issue\n\t\t\t\tif (!onlineDriveValue.empty) {\n\t\t\t\t\t// Ensure remoteDriveId is 16 characters long by padding with leading zeros if required\n\t\t\t\t\tif (onlineDriveValue.length < 16) {\n\t\t\t\t\t\t// online value is not 16 characters in length\n\t\t\t\t\t\t// Debug logging\n\t\t\t\t\t\tif (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\"]);}\n\t\t\t\t\t\t// Generate the change\n\t\t\t\t\t\tnewEntry = to!string(onlineDriveValue.padLeft('0', 16)); // Explicitly use padLeft for leading zero padding, leave case as-is\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Online value is 16 characters in length, use as-is\n\t\t\t\t\t\tnewEntry = onlineDriveValue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Debug Logging of result\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\" - old 'driveId' value = \" ~ oldEntry, [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\" - new 'driveId' value = \" ~ newEntry, [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Issue #3336 - Convert driveId to lowercase\n\t\t\t\t// Return the new calculated value as lowercase\n\t\t\t\treturn transformToLowerCase(newEntry);\n\t\t\t} else {\n\t\t\t\t// Display function processing time if configured to do so\n\t\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t\t// Combine module name & running Function\n\t\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t\t}\n\t\t\t\n\t\t\t\t// Issue #3336 - Convert driveId to lowercase\n\t\t\t\t// Return input value as-is as lowercase\n\t\t\t\treturn transformToLowerCase(objectParentDriveId);\n\t\t\t}\n\t\t} else {\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t\n\t\t\t// Issue #3336 - Convert driveId to lowercase\n\t\t\t// Return input value as-is as lowercase\n\t\t\treturn transformToLowerCase(objectParentDriveId);\n\t\t}\n\t}\n\t\n\t// Transform OneDrive Personal driveId or parentReference driveId to lowercase\n\tstring transformToLowerCase(string objectParentDriveId) {\n\t\t// Since 14 June 2025 (possibly earlier), the Microsoft Graph API has started returning inconsistent casing for driveId values across multiple OneDrive Personal API endpoints.\n\t\t// https://github.com/OneDrive/onedrive-api-docs/issues/1902\n\t\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\n\t\tstring transformedDriveIdValue;\n\t\ttransformedDriveIdValue = toLower(objectParentDriveId);\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return transformed value\n\t\treturn transformedDriveIdValue;\n\t}\n\t\n\t// Calculate the transfer metrics for the file to aid in performance discussions when they are raised\n\tvoid displayTransferMetrics(string fileTransferred, long transferredBytes, SysTime transferStartTime, SysTime transferEndTime) {\n\t\t// We only calculate this if 'display_transfer_metrics' is enabled or we are doing debug logging\n\t\tif (appConfig.getValueBool(\"display_transfer_metrics\") || debugLogging) {\n\t\t\n\t\t\t// Function Start Time\n\t\t\tSysTime functionStartTime;\n\t\t\tstring logKey;\n\t\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t\t// Only set this if we are generating performance processing times\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\t\tlogKey = generateAlphanumericString();\n\t\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t\t}\n\t\t\n\t\n\t\t\t// Calculations must be done on files > 0 transferredBytes\n\t\t\tif (transferredBytes > 0) {\n\t\t\t\t// Calculate transfer metrics\n\t\t\t\tauto transferDuration = transferEndTime - transferStartTime;\n\t\t\t\tdouble transferDurationAsSeconds = (transferDuration.total!\"msecs\"/1e3); // msec --> seconds\n\t\t\t\tdouble transferSpeedAsMbps = ((transferredBytes / transferDurationAsSeconds) / 1024 / 1024); // bytes --> Mbps\n\t\t\t\t\n\t\t\t\t// Output the transfer metrics\n\t\t\t\tstring transferMetrics = format(\"File: %s | Size: %d Bytes | Duration: %.2f Seconds | Speed: %.2f Mbps (approx)\", fileTransferred, transferredBytes, transferDurationAsSeconds, transferSpeedAsMbps);\n\t\t\t\taddLogEntry(\"Transfer Metrics - \" ~ transferMetrics);\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t// Zero bytes - not applicable\n\t\t\t\taddLogEntry(\"Transfer Metrics - N/A (Zero Byte File)\");\n\t\t\t}\n\t\t\t\n\t\t\t// Display function processing time if configured to do so\n\t\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t\t// Combine module name & running Function\n\t\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Recursively validate JSONValue for UTF-8 compliance\n\tbool validateUTF8JSON(in JSONValue json) {\n\t\tswitch (json.type) {\n\t\t\tcase JSONType.string:\n\t\t\t\treturn isValidUTF8(json.str);\n\t\t\tcase JSONType.array:\n\t\t\t\tforeach (ref item; json.array) {\n\t\t\t\t\tif (!validateUTF8JSON(item)) return false;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase JSONType.object:\n\t\t\t\tforeach (key, ref value; json.object) {\n\t\t\t\t\tif (!isValidUTF8(key) || !validateUTF8JSON(value)) return false;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak; // Other types (null, bool, int, float) don't need UTF-8 validation\n\t\t}\n\t\treturn true;\n\t}\n\t\n\t// Sanitise the provided onedriveJSONItem into a string that can actually be printed without error or issue\n\tstring sanitiseJSONItem(JSONValue onedriveJSONItem) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\t// Validate UTF-8 before serialisation\n\t\tif (!validateUTF8JSON(onedriveJSONItem)) {\n\t\t\treturn \"JSON Validation Failed: JSON data from OneDrive API contains invalid UTF-8 characters\";\n\t\t}\n\t\t\n\t\t// Redact PII in JSON before serialisation\n\t\tredactPII(onedriveJSONItem);\n\t\t\n\t\t// Eventual output variable\n\t\tstring sanitisedJSONString;\n\t\t\t\t\n\t\t// Try and serialise the JSON into a string\n\t\ttry {\n\t\t\tauto app = appender!string();\n\t\t\ttoJSON(app, onedriveJSONItem);\n\t\t\tsanitisedJSONString = app.data;\n\t\t} catch (Exception e) {\n\t\t\tsanitisedJSONString = \"JSON Serialisation Failed: \" ~ e.msg;\n\t\t}\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t\t\n\t\t// Return sanitised JSON string for logging output\n\t\treturn sanitisedJSONString;\n\t}\n\t\n\t// Recursively redact PII and sensitive elements from JSONValue\n\tvoid redactPII(ref JSONValue j) {\n\t\tif (j.type == JSONType.object) {\n\t\t\tforeach (key, ref value; j.object) {\n\n\t\t\t\t// Match Graph's actual keys directly\n\t\t\t\tif (key == \"email\") {\n\t\t\t\t\tvalue = JSONValue(\"<redacted-email>\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (key == \"displayName\") {\n\t\t\t\t\tvalue = JSONValue(\"<redacted-displayName>\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse\n\t\t\t\tredactPII(value);\n\t\t\t}\n\t\t} else if (j.type == JSONType.array) {\n\t\t\tforeach (ref value; j.array) {\n\t\t\t\tredactPII(value);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Obtain the Websocket Notification URL\n\tvoid obtainWebSocketNotificationURL() {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\tstring websocketURL;\n\t\t\n\t\t// Create a new API Instance for this thread and initialise it\n\t\tOneDriveApi queryWebsocketURLApiInstance;\n\t\tqueryWebsocketURLApiInstance = new OneDriveApi(appConfig);\n\t\tqueryWebsocketURLApiInstance.initialise();\n\t\t\n\t\t// Try and query Websocket Notification URL\n\t\ttry {\n\t\t\tJSONValue endpointResponse = queryWebsocketURLApiInstance.obtainWebSocketNotificationURL();\n\t\t\t\n\t\t\t// Was a valid JSON response provided?\n\t\t\tif (endpointResponse.type() == JSONType.object) {\n\t\t\t\t\n\t\t\t\t// Log response\n\t\t\t\tif (debugLogging) {addLogEntry(\"Response for a Socket.IO Subscription Endpoint: \" ~ to!string(endpointResponse), [\"debug\"]);}\n\t\t\t\t\n\t\t\t\t// Store the JSON in the configuration for reuse\n\t\t\t\tappConfig.websocketEndpointResponse = to!string(endpointResponse);\n\t\t\t\t\n\t\t\t\t// Extract and store the Notification URL from the response we received (no transformation)\n\t\t\t\twebsocketURL = endpointResponse[\"notificationUrl\"].str;\n\t\t\t\t\n\t\t\t\t// Extract and store the expiry\n\t\t\t\tappConfig.websocketUrlExpiry = endpointResponse[\"expirationDateTime\"].str;\n\t\t\t\tSysTime expiryUTC = SysTime.fromISOExtString(appConfig.websocketUrlExpiry);\n\t\t\t\tSysTime expiryLocal = expiryUTC.toLocalTime();\n\t\t\t\t\n\t\t\t\t// Do we have a valid Notification URL ?\n\t\t\t\tif (!websocketURL.empty) {\n\t\t\t\t\t// Store the websocket notification URL\n\t\t\t\t\tappConfig.websocketNotificationUrl = websocketURL;\n\t\t\t\t\t// Set flag\n\t\t\t\t\tappConfig.websocketNotificationUrlAvailable = true;\n\t\t\t\t\n\t\t\t\t\t// Log WebSocket specifics\n\t\t\t\t\tif (debugLogging) {\n\t\t\t\t\t\taddLogEntry(\"WebSocket Notification URL: \" ~ websocketURL, [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"WebSocket Expiry (UTC):     \" ~ to!string(expiryUTC), [\"debug\"]);\n\t\t\t\t\t\taddLogEntry(\"WebSocket Expiry (Local):   \" ~ to!string(expiryLocal), [\"debug\"]);\n\t\t\t\t\t}\n\t\t\t\t}\t\n\t\t\t}\n\t\t\t\n\t\t} catch (OneDriveException exception) {\n\t\t\t// An error, regardless of what it is ... not good\n\t\t\t// Display what the error is\n\t\t\t// - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance\n\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tqueryWebsocketURLApiInstance.releaseCurlEngine();\n\t\tqueryWebsocketURLApiInstance = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n\t\n\t// Download a single file via --download-file <path/to/file>\n\tvoid downloadSingleFile(string pathToQuery) {\n\t\t// Function Start Time\n\t\tSysTime functionStartTime;\n\t\tstring logKey;\n\t\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\t\t// Only set this if we are generating performance processing times\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\tfunctionStartTime = Clock.currTime();\n\t\t\tlogKey = generateAlphanumericString();\n\t\t\tdisplayFunctionProcessingStart(thisFunctionName, logKey);\n\t\t}\n\t\t\n\t\tOneDriveApi queryPathDetailsOnline;\n\t\tJSONValue onlinePathData;\n\t\t\n\t\t// Was a path to query passed in?\n\t\tif (pathToQuery.empty) {\n\t\t\t// Nothing to query\n\t\t\taddLogEntry(\"No path to query\");\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Create new OneDrive API Instance\n\t\tqueryPathDetailsOnline = new OneDriveApi(appConfig);\n\t\tqueryPathDetailsOnline.initialise();\n\t\t\n\t\ttry {\n\t\t\t// Query the OneDrive API, using the path, which will query 'our' OneDrive Account\n\t\t\tonlinePathData = queryPathDetailsOnline.getPathDetails(pathToQuery);\n\t\t\t\n\t\t\t\n\t\t} catch (OneDriveException exception) {\n\t\t\t\n\t\t\tif (exception.httpStatusCode == 404) {\n\t\t\t\t// Path does not exist online ...\n\t\t\t\taddLogEntry(\"ERROR: The requested path does not exist online. Please check for your file online.\");\n\t\t\t} else {\n\t\t\t\t// Display error message\n\t\t\t\tdisplayOneDriveErrorMessage(exception.msg, thisFunctionName);\n\t\t\t}\n\t\t\t\n\t\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\t\tqueryPathDetailsOnline.releaseCurlEngine();\n\t\t\tqueryPathDetailsOnline = null;\n\t\t\t// Perform Garbage Collection\n\t\t\tGC.collect();\n\t\t\t\n\t\t\t// Return .. nothing to do\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// Was a valid JSON response provided?\n\t\tif (onlinePathData.type() == JSONType.object) {\n\t\t\t// Valid JSON item was returned\n\t\t\t// Is the item a file ?\n\t\t\tif (isFileItem(onlinePathData)) {\n\t\t\t\t// JSON item is a file\n\t\t\t\t// Download the file based on the data returned\n\t\t\t\tdownloadFileItem(onlinePathData);\n\t\t\t} else {\n\t\t\t\t// The provided path is not a file\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: The requested path to download is not a file. Please correct this error and try again.\");\n\t\t\t\taddLogEntry();\n\t\t\t}\n\t\t} else {\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"ERROR: The requested file to download has generated an error. Please correct this error and try again.\");\n\t\t\taddLogEntry();\n\t\t}\n\t\t\n\t\t// OneDrive API Instance Cleanup - Shutdown API, free curl object and memory\n\t\tqueryPathDetailsOnline.releaseCurlEngine();\n\t\tqueryPathDetailsOnline = null;\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t\n\t\t// Display function processing time if configured to do so\n\t\tif (appConfig.getValueBool(\"display_processing_time\") && debugLogging) {\n\t\t\t// Combine module name & running Function\n\t\t\tdisplayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/util.d",
    "content": "// What is this module called?\nmodule util;\n\n// What does this module require to function?\nimport core.memory;\nimport core.stdc.errno : ENOENT, EINTR, EBUSY, EXDEV, EAGAIN, EPERM, EACCES, EROFS;\nimport core.stdc.stdlib;\nimport core.stdc.string;\nimport core.sys.posix.pwd;\nimport core.sys.posix.signal;\nimport core.sys.posix.sys.resource;\nimport core.sys.posix.sys.stat;\nimport core.sys.posix.unistd;\nimport core.thread;\nimport etc.c.curl;\nimport std.algorithm;\nimport std.array;\nimport std.ascii;\nimport std.base64;\nimport std.conv;\nimport std.datetime;\nimport std.digest.crc;\nimport std.digest.sha;\nimport std.exception;\nimport std.file;\nimport std.format;\nimport std.json;\nimport std.math;\nimport std.net.curl;\nimport std.path;\nimport std.process;\nimport std.random;\nimport std.range;\nimport std.regex;\nimport std.socket;\nimport std.stdio;\nimport std.string;\nimport std.traits;\nimport std.uri;\nimport std.utf;\n\n// What other modules that we have created do we need to import?\nimport log;\nimport config;\nimport qxor;\nimport curlEngine;\n\n// Global variable for the device name\n__gshared string deviceName;\n// Global flag for SIGINT (CTRL-C) and SIGTERM (kill) state\n__gshared bool exitHandlerTriggered = false;\n// Global variable for when we last uploaded something or made an online change from a local inotify event\n__gshared MonoTime lastLocalWrite;\n\n// util module variable\nulong previousRSS;\n\nstruct DesktopHints {\n    bool gnome;\n    bool kde;\n}\n\nshared static this() {\n\tdeviceName = Socket.hostName;\n}\n\n// To assist with filesystem severity issues, configure an enum that can be used\nenum FsErrorSeverity {\n\twarning,\n\terror,\n\tfatal,\n\tpermission\n}\n\n// Creates a safe backup of the given item, and only performs the function if not in a --dry-run scenario.\n// If the path already ends with \"-<deviceName>-safeBackup-####\", the counter is incremented\n// instead of appending another \"-<deviceName>-safeBackup-\".\nvoid safeBackup(const(char)[] path, bool dryRun, bool bypassDataPreservation, out string renamedPath) {\n    // Ensure this is currently null\n\trenamedPath = null;\n\tbool isDirectory = false;\n\t\n\t// If the path doesn’t exist, there is nothing to back up\n\tif (!exists(path)) {\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"safeBackup: Skipping backup as local path does not exist: \" ~ to!string(path), [\"debug\"]);\n\t\t}\n\t\treturn;\n\t}\n\n\t// Is the path a directory?\n\ttry {\n\t\tisDirectory = isDir(path);\n\t} catch (FileException e) {\n\t\t// Path disappeared or became inaccessible between exists() and isDir()\n\t\tif (verboseLogging) {\n\t\t\taddLogEntry(\"Path to backup no longer exists or is inaccessible: \" ~ to!string(path) ~ \" : \" ~ e.msg, [\"verbose\"]);\n\t\t}\n\t\t// Nothing left to back up — exit safely\n\t\treturn;\n\t}\n\t\n\t// Is the input path a folder|directory? These should never be renamed\n\tif (isDirectory) {\n\t\tif (verboseLogging) {\n\t\t\taddLogEntry(\"Renaming request of local directory is being ignored: \" ~ to!string(path), [\"verbose\"]);\n\t\t}\n\t\treturn;\n\t}\n\t\n\t// Has the user configured to IGNORE local data protection rules?\n    if (bypassDataPreservation) {\n        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\"]);\n        return;\n    }\n\t\n\t// Convert once for convenience\n    const string spath = to!string(path);\n    const string ext   = extension(spath);\n\n    // Compute stem without extension (handles no-extension case too)\n    const size_t stemLen = spath.length >= ext.length ? spath.length - ext.length : spath.length;\n    string stem = spath[0 .. stemLen];\n\n    // Tag used for our safe backups\n    string tag = \"-\" ~ deviceName ~ \"-safeBackup-\";\n\n    // Detect if already a tagged safeBackup on THIS device; if so, bump the 4-digit counter\n    int startN = 1;\n    string baseStem = stem;\n\n    if (stem.length >= tag.length + 4) {\n        // Slice out last 4 chars and the tag position\n        auto last4   = stem[$ - 4 .. $];\n        auto tagSpan = stem[$ - (tag.length + 4) .. $ - 4];\n\n        bool fourDigits = true;\n        foreach (c; last4) {\n            if (!c.isDigit) { fourDigits = false; break; }\n        }\n\n        if (fourDigits && tagSpan == tag) {\n            // Already a backup from this device — bump the counter\n            startN   = to!int(last4) + 1;\n            baseStem = stem[0 .. $ - (tag.length + 4)];\n        }\n    }\n\n    // Find the first available name, capped at 1000 attempts\n    int n = startN;\n    string candidate;\n\n    while (n <= 1000) {\n        candidate = baseStem ~ tag ~ format(\"%04d\", n) ~ ext;\n        if (!exists(candidate)) break;\n        ++n;\n    }\n\n    // If we exhausted our attempts, fail out\n    if (n > 1000) {\n        addLogEntry(\"Failed to backup \" ~ spath ~ \": Unique file name could not be found after 1000 attempts\", [\"error\"]);\n        return;\n    }\n\n    // Log intent\n    if (verboseLogging) {\n        addLogEntry(\"The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent local data loss: \" ~ spath ~ \" -> \" ~ candidate, [\"verbose\"]);\n    }\n\n    // Perform (or simulate) the rename\n    if (!dryRun) {\n\t\t// Not a --dry-run scenario - attempt the file rename to create a safe backup\n\t\t// Use safeRename()\n\t\tif (safeRename(spath, candidate, dryRun)) {\n\t\t\trenamedPath = candidate;\n\t\t} else {\n\t\t\t// Failed to rename using safeRename()\n\t\t\taddLogEntry(\"Renaming of local file failed for \" ~ spath ~ \" -> \" ~ candidate, [\"error\"]);\n\t\t}\n    } else {\n        if (debugLogging) {\n            addLogEntry(\"DRY-RUN: Skipping renaming local file to preserve existing file and prevent data loss: \" ~ spath ~ \" -> \" ~ candidate, [\"debug\"]);\n        }\n    }\n}\n\n// Rename the given item, and only performs the function if not in a --dry-run scenario\nbool safeRename(const(char)[] oldPath, const(char)[] newPath, bool dryRun) {\n\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__), strip(getFunctionName!({})));\n\n\tif (dryRun) {\n\t\tif (debugLogging) { addLogEntry(\"DRY-RUN: Skipping local file rename\", [\"debug\"]); }\n\t\treturn true;\n\t}\n\n\tint maxAttempts = 5;\n\n\tforeach (attempt; 0 .. maxAttempts) {\n\t\ttry {\n\t\t\tif (debugLogging) { addLogEntry(\"Calling rename(oldPath, newPath)\", [\"debug\"]); }\n\t\t\t\n\t\t\t// There are 2 options to rename a file\n\t\t\t// rename() - https://dlang.org/library/std/file/rename.html\n\t\t\t// std.file.copy() - https://dlang.org/library/std/file/copy.html\n\t\t\t//\n\t\t\t// rename:\n\t\t\t//   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.\n\t\t\t//\n\t\t\t// std.file.copy\n\t\t\t//   Copy file from to file to. File timestamps are preserved. File attributes are preserved, if preserve equals Yes.preserveAttributes\n\t\t\t//\n\t\t\t// Use rename() as Linux is POSIX compliant, we have an atomic operation where at no point in time the 'to' is missing.\n\t\t\t\t\t\t\n\t\t\trename(oldPath, newPath);\n\t\t\treturn true;\n\t\t} catch (FileException e) {\n\t\t\t// Retry on EINTR\n\t\t\tif (e.errno == EINTR) {        // Interrupted by signal → retry\n\t\t\t\t// 10ms backoff to avoid spinning if signals are frequent\n\t\t\t\tThread.sleep(dur!\"msecs\"(10 * (attempt + 1)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Retry on EBUSY\n\t\t\tif (e.errno == EBUSY) {        // Filesystem was busy → retry\n\t\t\t\t// 25ms backoff to avoid spinning if signals are frequent\n\t\t\t\tThread.sleep(dur!\"msecs\"(25 * (attempt + 1)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Cross-device rename: not retryable\n\t\t\tif (e.errno == EXDEV) {\n\t\t\t\tdisplayFileSystemErrorMessage(\"Rename failed (cross-filesystem): \" ~ e.msg, thisFunctionName, \"oldPath=\" ~ to!string(oldPath) ~ \" newPath=\" ~ to!string(newPath));\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t// Everything else: log once and return\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, \"oldPath=\" ~ to!string(oldPath) ~ \" newPath=\" ~ to!string(newPath));\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// If we get here, we exhausted retries\n\t// Log the last failure\n\tdisplayFileSystemErrorMessage(\"Failed to rename after retries: \", thisFunctionName, \"oldPath=\" ~ to!string(oldPath) ~ \" newPath=\" ~ to!string(newPath));\n\treturn false;\n}\n\n// Deletes the specified file without throwing an exception if there is an issue\nvoid safeRemove(const(char)[] path) {\n\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__), strip(getFunctionName!({})));\n\n\tint maxAttempts = 5;\n\n\tforeach (attempt; 0 .. maxAttempts) {\n\t\ttry {\n\t\t\t// Attempt to remove; no pre-check to avoid TOCTTOU\n\t\t\tremove(path);\n\t\t\treturn;\n\t\t} catch (FileException e) {\n\t\t\tif (e.errno == ENOENT) return; // already gone → fine\n\t\t\t// Retry on EINTR\n\t\t\tif (e.errno == EINTR) {        // Interrupted by signal → retry\n\t\t\t\t// 10ms backoff to avoid spinning if signals are frequent\n\t\t\t\tThread.sleep(dur!\"msecs\"(10 * (attempt + 1)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Retry on EBUSY\n\t\t\tif (e.errno == EBUSY) {        // Filesystem was busy → retry\n\t\t\t\t// 25ms backoff to avoid spinning if signals are frequent\n\t\t\t\tThread.sleep(dur!\"msecs\"(25 * (attempt + 1)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Anything else is noteworthy (EISDIR, EACCES, etc.)\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, to!string(path));\n\t\t\treturn;\n\t\t}\n\t}\n\t// If we get here, we exhausted retries\n\t// Log the last failure\n\tdisplayFileSystemErrorMessage(\"Failed to remove file after retries: \" ~ to!string(path), thisFunctionName, to!string(path));\n}\n\n// Returns the quickXorHash base64 string of a file, or an empty string on failure\nstring computeQuickXorHash(string path) {\n\n    QuickXor qxor;\n    File file;\n    bool fileOpened = false;\n\n    scope(exit) {\n        if (fileOpened) {\n            file.close();\n        }\n    }\n\n    try {\n        // Open file for reading\n        file = File(path, \"rb\");\n        fileOpened = true;\n\n        // Single stat call for BOTH size and preferred block size\n        ulong fs = 0;\n        size_t blockSize = 4096; // sensible default\n\n        try {\n            auto de = DirEntry(path);\n            auto st = de.statBuf; // POSIX stat struct inferred\n\n            if (st.st_size > 0)\n                fs = cast(ulong) st.st_size;\n\n            if (st.st_blksize > 0)\n                blockSize = cast(size_t) st.st_blksize;\n        } catch (Exception e) {\n            // Best-effort only; keep defaults if stat fails\n            addLogEntry(\"Unexpected error while stat'ing file for hash sizing: \" ~ path ~ \" - \" ~ e.msg);\n        }\n\n        // Choose factor based on file size\n        size_t factor;\n        if (fs == 0) {\n            factor = 256;                                  // unknown size -> moderate buffer\n        } else if (fs < 1_048_576UL) {                     // < 1 MiB\n            factor = 16;                                   // small buffer\n        } else if (fs < 1_073_741_824UL) {                 // < 1 GiB\n            factor = 256;                                  // medium buffer\n        } else {                                           // >= 1 GiB\n            factor = 512;                                  // larger buffer\n        }\n\n        // Compute bufSize and clamp to [64 KiB, 8 MiB]\n        size_t bufSize = blockSize * factor;\n\n        if (bufSize < 64 * 1024)\n            bufSize = 64 * 1024;\n        if (bufSize > 8 * 1024 * 1024)\n            bufSize = 8 * 1024 * 1024;\n\n        // Allocate outside GC to avoid scanning big buffers\n        auto raw = cast(ubyte*) malloc(bufSize);\n        if (raw is null) {\n            addLogEntry(\"Failed to compute QuickXor Hash for file: \" ~ path ~ \" - out of memory allocating buffer\");\n            return \"\";\n        }\n        scope(exit) free(raw);\n\n        ubyte[] buf = raw[0 .. bufSize];\n\n        // Large sequential reads, minimal syscall overhead\n        for (;;) {\n            auto chunk = file.rawRead(buf);   // returns slice of bytes read\n            if (chunk.length == 0) break;     // EOF\n            qxor.put(chunk);\n        }\n\n    } catch (ErrnoException e) {\n        addLogEntry(\"Failed to compute QuickXor Hash for file: \" ~ path ~ \" - \" ~ e.msg);\n        return \"\";\n    } catch (Exception e) {\n        addLogEntry(\"Unexpected error while computing QuickXor Hash for file: \" ~ path ~ \" - \" ~ e.msg);\n        return \"\";\n    }\n\n    auto hashResult = qxor.finish();\n\treturn Base64.encode(hashResult).idup;\n}\n\n// Returns the SHA256 hash hex string of a file, or an empty string on failure\nstring computeSHA256Hash(string path) {\n\n    SHA256 sha256;\n    File file;\n    bool fileOpened = false;\n\n    scope(exit) {\n        if (fileOpened) {\n            file.close();\n        }\n    }\n\n    try {\n        // Open file for reading\n        file = File(path, \"rb\");\n        fileOpened = true;\n\n        // Single stat call for BOTH size and preferred block size\n        ulong fs = 0;\n        size_t blockSize = 4096; // sensible default\n\n        try {\n            auto de = DirEntry(path);\n            auto st = de.statBuf; // POSIX stat struct inferred\n\n            if (st.st_size > 0)\n                fs = cast(ulong) st.st_size;\n\n            if (st.st_blksize > 0)\n                blockSize = cast(size_t) st.st_blksize;\n        } catch (Exception e) {\n            // Best-effort only; keep defaults if stat fails\n            addLogEntry(\"Unexpected error while stat'ing file for hash sizing: \" ~ path ~ \" - \" ~ e.msg);\n        }\n\n        // Choose factor based on file size\n        size_t factor;\n        if (fs == 0) {\n            factor = 256;                                  // unknown size -> moderate buffer\n        } else if (fs < 1_048_576UL) {                     // < 1 MiB\n            factor = 16;                                   // small buffer\n        } else if (fs < 1_073_741_824UL) {                 // < 1 GiB\n            factor = 256;                                  // medium buffer\n        } else {                                           // >= 1 GiB\n            factor = 512;                                  // larger buffer\n        }\n\n        // Compute bufSize and clamp to [64 KiB, 8 MiB]\n        size_t bufSize = blockSize * factor;\n\n        if (bufSize < 64 * 1024)\n            bufSize = 64 * 1024;\n        if (bufSize > 8 * 1024 * 1024)\n            bufSize = 8 * 1024 * 1024;\n\n        // Allocate outside GC to avoid scanning big buffers\n        auto raw = cast(ubyte*) malloc(bufSize);\n        if (raw is null) {\n            addLogEntry(\"Failed to compute SHA256 Hash for file: \" ~ path ~ \" - out of memory allocating buffer\");\n            return \"\";\n        }\n        scope(exit) free(raw);\n\n        ubyte[] buf = raw[0 .. bufSize];\n\n        // Large sequential reads, minimal syscall overhead\n        for (;;) {\n            auto chunk = file.rawRead(buf);   // returns slice of bytes read\n            if (chunk.length == 0) break;     // EOF\n            sha256.put(chunk);\n        }\n\n    } catch (ErrnoException e) {\n        addLogEntry(\"Failed to compute SHA256 Hash for file: \" ~ path ~ \" - \" ~ e.msg);\n        return \"\";\n    } catch (Exception e) {\n        addLogEntry(\"Unexpected error while computing SHA256 Hash for file: \" ~ path ~ \" - \" ~ e.msg);\n        return \"\";\n    }\n\n    auto hashResult = sha256.finish();\n\treturn toHexString(hashResult).idup;\n}\n\n// Converts wildcards (*, ?) to regex\n// The changes here need to be 100% regression tested before full release\nRegex!char wild2regex(const(char)[] pattern) {\n    string str;\n    str.reserve(pattern.length + 2);\n    str ~= \"^\";\n    foreach (c; pattern) {\n        switch (c) {\n        case '*':\n            str ~= \".*\";  // Changed to match any character. Was:      str ~= \"[^/]*\";\n            break;\n        case '.':\n            str ~= \"\\\\.\";\n            break;\n        case '?':\n            str ~= \".\";  // Changed to match any single character. Was:    str ~= \"[^/]\";\n            break;\n        case '|':\n            str ~= \"$|^\";\n            break;\n        case '+':\n            str ~= \"\\\\+\";\n            break;\n        case ' ':\n            str ~= \"\\\\s\";  // Changed to match exactly one whitespace. Was:   str ~= \"\\\\s+\";\n            break;  \n        case '/':\n            str ~= \"\\\\/\";\n            break;\n        case '(':\n            str ~= \"\\\\(\";\n            break;\n        case ')':\n            str ~= \"\\\\)\";\n            break;\n        default:\n            str ~= c;\n            break;\n        }\n    }\n    str ~= \"$\";\n    return regex(str, \"i\");\n}\n\n// Test Internet access to Microsoft OneDrive using a simple HTTP HEAD request\nbool testInternetReachability(ApplicationConfig appConfig, bool displayLogging = true) {\n\tHTTP http = HTTP();\n\thttp.url = \"https://login.microsoftonline.com\";\n\t\n\t// Configure timeouts based on application configuration\n\thttp.dnsTimeout = dur!\"seconds\"(appConfig.getValueLong(\"dns_timeout\"));\n\thttp.connectTimeout = dur!\"seconds\"(appConfig.getValueLong(\"connect_timeout\"));\n\thttp.dataTimeout = dur!\"seconds\"(appConfig.getValueLong(\"data_timeout\"));\n\thttp.operationTimeout = dur!\"seconds\"(appConfig.getValueLong(\"operation_timeout\"));\n\n\t// Set IP protocol version\n\thttp.handle.set(CurlOption.ipresolve, appConfig.getValueLong(\"ip_protocol_version\"));\n\n\t// Explicitly set libcurl options to avoid using signal handlers in a multi-threaded environment\n\t//   https://curl.se/libcurl/c/CURLOPT_NOSIGNAL.html\n\thttp.handle.set(CurlOption.nosignal,1);\n\t\n\t// Explicitly set the use of TCP NAGLE\n\t//   https://curl.se/libcurl/c/CURLOPT_TCP_NODELAY.html\n\t//   Ensure that TCP_NODELAY is set to 0 to ensure that TCP NAGLE is enabled\n\thttp.handle.set(CurlOption.tcp_nodelay,0);\n\t\n\t// Explicitly set to ensure libcurl keep the connection open for possible later reuse\n\t//   https://curl.se/libcurl/c/CURLOPT_FORBID_REUSE.html\n\thttp.handle.set(CurlOption.forbid_reuse,0);\n\t\n\t// Set HTTP method to HEAD for minimal data transfer\n\thttp.method = HTTP.Method.head;\n\t\n\tbool reachedService = false;\n\t\n\t// Exit scope to ensure cleanup http object\n\tscope(exit) {\n\t\t// Shut http down http object\n\t\thttp.shutdown();\n\t}\n\n\t// Execute the request and handle exceptions\n\ttry {\n\t\tif (displayLogging) {\n\t\t\taddLogEntry(\"Attempting to contact the Microsoft OneDrive Service\");\n\t\t}\n\t\thttp.perform();\n\n\t\t// Check response for HTTP status code - consider 2xx and 3xx as \"reachable\"\n\t\tif (http.statusLine.code >= 200 && http.statusLine.code < 400) {\n\t\t\tif (displayLogging) {\n\t\t\t\taddLogEntry(\"Successfully reached the Microsoft OneDrive Service\");\n\t\t\t}\n\t\t\treachedService = true;\n\t\t} else {\n\t\t\taddLogEntry(\"Failed to reach the Microsoft OneDrive Service. HTTP status code: \" ~ to!string(http.statusLine.code));\n\t\t\treachedService = false;\n\t\t}\n\t} catch (SocketException e) {\n\t\taddLogEntry(\"Cannot connect to the Microsoft OneDrive Service - Socket Issue: \" ~ e.msg);\n\t\tdisplayOneDriveErrorMessage(e.msg, getFunctionName!({}));\n\t\treachedService = false;\n\t} catch (CurlException e) {\n\t\taddLogEntry(\"Cannot connect to the Microsoft OneDrive Service - Network Connection Issue: \" ~ e.msg);\n\t\tdisplayOneDriveErrorMessage(e.msg, getFunctionName!({}));\n\t\treachedService = false;\n\t} catch (Exception e) {\n\t\taddLogEntry(\"An unexpected error occurred: \" ~ e.toString());\n\t\tdisplayOneDriveErrorMessage(e.toString(), getFunctionName!({}));\n\t\treachedService = false;\n\t}\n\t\n\t// Return state\n\treturn reachedService;\n}\n\n// Retry Internet access test to Microsoft OneDrive\nbool retryInternetConnectivityTest(ApplicationConfig appConfig) {\n    int retryAttempts = 0;\n    int backoffInterval = 1; // initial backoff interval in seconds\n    int maxBackoffInterval = 3600; // maximum backoff interval in seconds\n    int maxRetryCount = 100; // max retry attempts, reduced for practicality\n    bool isOnline = false;\n\n    while (retryAttempts < maxRetryCount && !isOnline) {\n        if (backoffInterval < maxBackoffInterval) {\n            backoffInterval = min(backoffInterval * 2, maxBackoffInterval); // exponential increase\n        }\n\n        if (debugLogging) {\n\t\t\taddLogEntry(\"  Retry Attempt:      \" ~ to!string(retryAttempts + 1), [\"debug\"]);\n\t\t\taddLogEntry(\"  Retry In (seconds): \" ~ to!string(backoffInterval), [\"debug\"]);\n\t\t}\n\n        Thread.sleep(dur!\"seconds\"(backoffInterval));\n        isOnline = testInternetReachability(appConfig); // assuming this function is defined elsewhere\n\n        if (isOnline) {\n            addLogEntry(\"Internet connectivity to Microsoft OneDrive service has been restored\");\n        }\n\n        retryAttempts++;\n    }\n\n    if (!isOnline) {\n        addLogEntry(\"ERROR: Was unable to reconnect to the Microsoft OneDrive service after \" ~ to!string(maxRetryCount) ~ \" attempts!\");\n    }\n\t\n\t// Return state\n    return isOnline;\n}\n\n// Can we read the local file - as a permissions issue or file corruption will cause a failure\n// https://github.com/abraunegg/onedrive/issues/113\n// returns true if file can be accessed\nbool readLocalFile(string path) {\n\t// Set this function name\n\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__) , strip(getFunctionName!({})));\n\n    // What is the file size\n\tif (getSize(path) != 0) {\n\t\ttry {\n\t\t\t// Attempt to read up to the first 1 byte of the file\n\t\t\tauto data = read(path, 1);\n\n\t\t\t// Check if the read operation was successful\n\t\t\tif (data.length != 1) {\n\t\t\t\t// Read operation not successful\n\t\t\t\taddLogEntry(\"Failed to read the required amount from the file: \" ~ path);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch (std.file.FileException e) {\n\t\t\t// Unable to read the file, log the error message\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, path);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t} else {\n\t\t// zero byte files cannot be read, return true\n\t\treturn true;\n\t}\n}\n\n// Calls globMatch for each string in pattern separated by '|'\nbool multiGlobMatch(const(char)[] path, const(char)[] pattern) {\n    if (path.length == 0 || pattern.length == 0) {\n        return false;\n    }\n\n    if (!pattern.canFind('|')) {\n        return globMatch!(std.path.CaseSensitive.yes)(path, pattern);\n    }\n\n    foreach (glob; pattern.split('|')) {\n        if (globMatch!(std.path.CaseSensitive.yes)(path, glob)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n// Check if the provided item name is a reserved Microsoft / Windows device name\n// This must catch both:\n//   - exact reserved names, e.g. \"CON\"\n//   - reserved names followed by an extension, e.g. \"CON.txt\", \"NUL.tar.gz\"\n// Microsoft documents that reserved names remain invalid even when followed by an extension.\nbool isReservedMicrosoftName(string itemName, const(bool[string]) disallowedSet) {\n\t// Ensure case-insensitive comparisons\n\tstring candidate = itemName.toLower();\n\n\t// Exact match\n\tif (disallowedSet.get(candidate, false)) {\n\t\treturn true;\n\t}\n\n\t// Reserved device names followed by an extension, e.g. \"CON.txt\"\n\tauto firstDot = countUntil(candidate, \".\");\n\tif (firstDot > 0) {\n\t\tstring deviceRoot = candidate[0 .. firstDot];\n\t\tif (disallowedSet.get(deviceRoot, false)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\n// Does the path pass the Microsoft restriction and limitations about naming files and folders\nbool isValidName(string path) {\n\t// Restriction and limitations about windows naming files and folders\n\t// https://msdn.microsoft.com/en-us/library/aa365247\n\t// https://support.microsoft.com/en-us/help/3125202/restrictions-and-limitations-when-you-sync-files-and-folders\n\t\n\tif (path == \".\") {\n\t\treturn true;\n\t}\n\n\tstring itemName = baseName(path).toLower(); // Ensure case-insensitivity\n\n\t// Check for explicitly disallowed names\n\t// 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\n\tstring[] disallowedNames = [\n\t\t\".lock\", \"desktop.ini\", \"CON\", \"PRN\", \"AUX\", \"NUL\",\n\t\t\"COM0\", \"COM1\", \"COM2\", \"COM3\", \"COM4\", \"COM5\", \"COM6\", \"COM7\", \"COM8\", \"COM9\",\n\t\t\"LPT0\", \"LPT1\", \"LPT2\", \"LPT3\", \"LPT4\", \"LPT5\", \"LPT6\", \"LPT7\", \"LPT8\", \"LPT9\"\n\t];\n\n\t// Creating an associative array for faster lookup\n\tbool[string] disallowedSet;\n\tforeach (name; disallowedNames) {\n\t\tdisallowedSet[name.toLower()] = true; // Normalise to lowercase\n\t}\n\n\tif (isReservedMicrosoftName(itemName, disallowedSet) || itemName.startsWith(\"~$\") || canFind(itemName, \"_vti_\")) {\n\t\treturn false;\n\t}\n\n\t// Regular expression for invalid patterns\n\t// 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\n\t// Leading whitespace and trailing whitespace\n\t// Invalid characters\n\t// Trailing dot '.' (not documented above) , however see issue https://github.com/abraunegg/onedrive/issues/2678\n\t\n\t//auto invalidNameReg = ctRegex!(`^\\s.*|^.*[\\s\\.]$|.*[<>:\"\\|\\?*/\\\\].*`); - original to remove at some point\n\tauto invalidNameReg = ctRegex!(`^\\s+|\\s$|\\.$|[<>:\"\\|\\?*/\\\\]`); // revised 25/3/2024\n\t// - ^\\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.\n\t// - \\s$ matches a whitespace character at the end of the string. This is more precise than [\\s\\.]$ because we'll handle the dot separately.\n\t// -  \\.$ specifically matches a dot character at the end of the string, addressing the requirement to catch trailing dots as invalid.\n\t// - [<>:\"\\|\\?*/\\\\] matches any single instance of the specified invalid characters: \", *, :, <, >, ?, /, \\, |\n\n\tauto matchResult = match(itemName, invalidNameReg);\n\tif (!matchResult.empty) {\n\t\treturn false;\n\t}\n\n\t// Determine if the path is at the root level, if yes, check that 'forms' is not the first folder\n\tauto segments = pathSplitter(path).array;\n\tif (segments.length <= 2 && segments.back.toLower() == \"forms\") { // Check only the last segment, convert to lower as OneDrive is not POSIX compliant, easier to compare\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n// Does the path contain any bad whitespace characters\nbool containsBadWhiteSpace(string path) {\n    // Check for null or empty string\n    if (path.length == 0) {\n        return false;\n    }\n\n    // Check for root item\n    if (path == \".\") {\n        return false;\n    }\n\t\n\t// https://github.com/abraunegg/onedrive/issues/35\n\t// Issue #35 presented an interesting issue where the filename contained a newline item\n\t//\t\t'State-of-the-art, challenges, and open issues in the integration of Internet of'$'\\n''Things and Cloud Computing.pdf'\n\t// When the check to see if this file was present the GET request queries as follows:\n\t//\t\t/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\n\t// The '$'\\n'' is translated to %0A which causes the OneDrive query to fail\n\t// Check for the presence of '%0A' via regex\n\n    string itemName = encodeComponent(baseName(path));\n    // Check for encoded newline character\n    return itemName.indexOf(\"%0A\") != -1;\n}\n\n// Does the path contain any ASCII HTML Codes\nbool containsASCIIHTMLCodes(string path) {\n\t// Check for null or empty string\n    if (path.length == 0) {\n        return false;\n    }\n\n    // Check for root item\n    if (path == \".\") {\n        return false;\n    }\n\n\t// https://github.com/abraunegg/onedrive/issues/151\n\t// If a filename contains ASCII HTML codes, it generates an error when attempting to upload this to Microsoft OneDrive\n\t// Check if the filename contains an ASCII HTML code sequence\n\n\t// Check for the pattern &# followed by 1 to 4 digits and a semicolon\n\tauto invalidASCIICode = ctRegex!(`&#[0-9]{1,4};`);\n\n\t// Use match to search for ASCII HTML codes in the path\n\tauto matchResult = match(path, invalidASCIICode);\n\n\t// Return true if ASCII HTML codes are found\n\treturn !matchResult.empty;\n}\n\n// Does the path contain any ASCII Control Codes\nbool containsASCIIControlCodes(string path) {\n    // Check for null or empty string\n    if (path.length == 0) {\n        return false;\n    }\n\n    // Check for root item\n    if (path == \".\") {\n        return false;\n    }\n\n    // https://github.com/abraunegg/onedrive/discussions/2553#discussioncomment-7995254\n\t//  Define a ctRegex pattern for ASCII control codes and specific non-ASCII control characters\n    //  This pattern includes the ASCII control range and common non-ASCII control characters\n    //  Adjust the pattern as needed to include specific characters of concern\n\tauto controlCodePattern = ctRegex!(`[\\x00-\\x1F\\x7F]|\\p{Cc}`); // Blocks ƒ†¯~‰ (#2553) , allows α (#2598)\n\n    // Use match to search for ASCII control codes in the path\n    auto matchResult = match(path, controlCodePattern);\n\n    // Return true if matchResult is not empty (indicating a control code was found)\n    return !matchResult.empty;\n}\n\n// Is the string a valid UTF-8 timestamp string?\nbool isValidUTF8Timestamp(string input) {\n\ttry {\n\t\t// Validate the entire string for UTF-8 correctness\n\t\tvalidate(input); // Throws UTFException if invalid UTF-8 is found\n\n\t\t// Validate the input against UTF-8 test cases\n\t\tif (!isValidUTF8(input)) {\n\t\t\t// error message already printed\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// Additional edge-case handling because the input format is known and controlled:\n\t\t// Ensure input length is within the expected range for a UTC datetime\n\t\tif (input.length < 20 || input.length > 30) {\n\t\t\t// not the correct length\n\t\t\taddLogEntry(\"UTF-8 validation failed: Input '\" ~ input ~ \"' is not within the expected length range for UTC datetime strings (20-30 characters).\");\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t} catch (UTFException) {\n\t\taddLogEntry(\"UTF-8 validation failed: Input '\" ~ input ~ \"' contains invalid UTF-8 characters.\");\n\t\treturn false;\n\t}\n}\n\n// Is the string a valid UTF-8 string?\nbool isValidUTF8(string input) {\n\ttry {\n\t\t// Validate the entire string for UTF-8 correctness\n\t\tvalidate(input); // Throws UTFException if invalid UTF-8 is found\n\n\t\t// Iterate through each character using byUTF to ensure proper UTF-8 decoding\n\t\tauto it = input.byUTF!(char);\n\t\tforeach (_; it) {\n\t\t\t// Iterating over the range ensures every UTF-8 sequence in the string is decoded into valid `dchar`s.\n\t\t\t// Throws a UTFException if an invalid UTF-8 sequence is encountered during decoding.\n\t\t}\n\n\t\t// Check for replacement characters\n\t\tif (input.count!((dchar c) => c == '\\uFFFD') > 0) {\n\t\t\t// contains replacement character\n\t\t\taddLogEntry(\"UTF-8 validation failed: Input contains replacement characters (�).\");\n\t\t\treturn false;\n\t\t}\n\n\t\t// return true\n\t\treturn true;\n\t} catch (UTFException) {\n\t\taddLogEntry(\"UTF-8 validation failed: Input '\" ~ input ~ \"' contains invalid UTF-8 characters.\");\n\t\treturn false;\n\t}\n}\n\n// Is the path a valid UTF-16 encoded path?\nbool isValidUTF16(string path) {\n    // Check for null or empty string\n    if (path.length == 0) {\n        return true;\n    }\n\n    // Check for root item\n    if (path == \".\") {\n        return true;\n    }\n\n    auto wpath = toUTF16(path); // Convert to UTF-16 encoding\n    auto it = wpath.byCodeUnit;\n\n    while (!it.empty) {\n        ushort current = it.front;\n        \n        // Check for valid single unit\n        if (current <= 0xD7FF || (current >= 0xE000 && current <= 0xFFFF)) {\n            it.popFront();\n        }\n        // Check for valid surrogate pair\n        else if (current >= 0xD800 && current <= 0xDBFF) {\n            it.popFront();\n            if (it.empty || it.front < 0xDC00 || it.front > 0xDFFF) {\n                return false; // Invalid surrogate pair\n            }\n            it.popFront();\n        } else {\n            return false; // Invalid code unit\n        }\n    }\n\n    return true;\n}\n\n// Validate that the provided string is a valid date time stamp in UTC format\nbool isValidUTCDateTime(string dateTimeString) {\n    // Regular expression for validating the string against UTC datetime format\n\t// Allows for an optional fractional second part (e.g., .123 or .123456789)\n\tauto pattern = regex(r\"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$\");\n\t\t\n\t// Validate for UTF-8 first\n\tif (!isValidUTF8Timestamp(dateTimeString)) {\n\t\tif (dateTimeString.empty) {\n\t\t\t// empty string\n\t\t\taddLogEntry(\"BAD TIMESTAMP (UTF-8 FAIL): empty string\");\n\t\t} else {\n\t\t\t// log string that caused UTF-8 failure\n\t\t\taddLogEntry(\"BAD TIMESTAMP (UTF-8 FAIL): \" ~ dateTimeString);\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t// First, check if the string matches the pattern\n\tif (!match(dateTimeString, pattern)) {\n\t\taddLogEntry(\"BAD TIMESTAMP (REGEX FAIL): \" ~ dateTimeString);\n\t\treturn false;\n\t}\n\n\t// Attempt to parse the string into a DateTime object\n\ttry {\n\t\tauto dt = SysTime.fromISOExtString(dateTimeString);\n\t\treturn true;\n\t} catch (TimeException) {\n\t\taddLogEntry(\"BAD TIMESTAMP (CONVERSION FAIL): \" ~ dateTimeString);\n\t\treturn false;\n\t}\n}\n\n// Does the path contain any HTML URL encoded items (e.g., '%20' for space)\nbool containsURLEncodedItems(string path) {\n    // Check for null or empty string\n    if (path.length == 0) {\n        return false;\n    }\n\n    // Pattern for percent encoding: % followed by two hexadecimal digits\n    auto urlEncodedPattern = ctRegex!(`%[0-9a-fA-F]{2}`);\n\n    // Search for URL encoded items in the string\n    auto matchResult = match(path, urlEncodedPattern);\n\n    // Return true if URL encoded items are found\n    return !matchResult.empty;\n}\n\n// Parse and display error message received from OneDrive\nvoid displayOneDriveErrorMessage(string message, string callingFunction) {\n\taddLogEntry();\n\taddLogEntry(\"ERROR: Microsoft OneDrive API returned an error with the following message:\");\n\tauto errorArray = splitLines(message);\n\taddLogEntry(\"  Error Message:       \" ~ to!string(errorArray[0]));\n\t// Extract 'message' as the reason\n\tJSONValue errorMessage = parseJSON(replace(message, errorArray[0], \"\"));\n\t\n\t// What is the reason for the error\n\tif (errorMessage.type() == JSONType.object) {\n\t\t// configure the error reason\n\t\tstring errorReason;\n\t\tstring errorCode;\n\t\tstring requestDate;\n\t\tstring requestId;\n\t\tstring localizedMessage;\n\t\t\n\t\t// set the reason for the error\n\t\ttry {\n\t\t\t// Use error_description as reason\n\t\t\terrorReason = errorMessage[\"error_description\"].str;\n\t\t} catch (JSONException e) {\n\t\t\t// we dont want to do anything here\n\t\t}\n\t\t\n\t\t// set the reason for the error\n\t\ttry {\n\t\t\t// Use [\"error\"][\"message\"] as reason\n\t\t\terrorReason = errorMessage[\"error\"][\"message\"].str;\t\n\t\t} catch (JSONException e) {\n\t\t\t// we dont want to do anything here\n\t\t}\n\t\t\n\t\t// Microsoft has started adding 'localizedMessage' to error JSON responses. If this is available, use this\n\t\ttry {\n\t\t\t// Use [\"error\"][\"localizedMessage\"] as localised reason\n\t\t\tlocalizedMessage = errorMessage[\"error\"][\"localizedMessage\"].str;\t\n\t\t} catch (JSONException e) {\n\t\t\t// we dont want to do anything here if not available\n\t\t}\n\t\t\n\t\t// Display the error reason\n\t\tif (errorReason.startsWith(\"<!DOCTYPE\")) {\n\t\t\t// a HTML Error Reason was given\n\t\t\taddLogEntry(\"  Error Reason:        A HTML Error response was provided. Use debug logging (--verbose --verbose) to view this error\");\n\t\t\tif (debugLogging) {addLogEntry(errorReason, [\"debug\"]);}\n\t\t\t\n\t\t} else {\n\t\t\t// a non HTML Error Reason was given\n\t\t\taddLogEntry(\"  Error Reason:        \" ~ errorReason);\n\t\t}\n\t\t\n\t\t// Get the error code if available\n\t\ttry {\n\t\t\t// Use [\"error\"][\"code\"] as code\n\t\t\terrorCode = errorMessage[\"error\"][\"code\"].str;\n\t\t} catch (JSONException e) {\n\t\t\t// we dont want to do anything here\n\t\t}\n\t\t\n\t\t// Get the date of request if available\n\t\ttry {\n\t\t\t// Use [\"error\"][\"innerError\"][\"date\"] as date\n\t\t\trequestDate = errorMessage[\"error\"][\"innerError\"][\"date\"].str;\t\n\t\t} catch (JSONException e) {\n\t\t\t// we dont want to do anything here\n\t\t}\n\t\t\n\t\t// Get the request-id if available\n\t\ttry {\n\t\t\t// Use [\"error\"][\"innerError\"][\"request-id\"] as request-id\n\t\t\trequestId = errorMessage[\"error\"][\"innerError\"][\"request-id\"].str;\t\n\t\t} catch (JSONException e) {\n\t\t\t// we dont want to do anything here\n\t\t}\n\t\t\n\t\t// Display the localizedMessage, error code, date and request id if available\n\t\tif (localizedMessage != \"\")   addLogEntry(\"  Error Reason (L10N): \" ~ localizedMessage);\n\t\tif (errorCode != \"\")   addLogEntry(\"  Error Code:          \" ~ errorCode);\n\t\tif (requestDate != \"\") addLogEntry(\"  Error Timestamp:     \" ~ requestDate);\n\t\tif (requestId != \"\")   addLogEntry(\"  API Request ID:      \" ~ requestId);\t\t   \n\t}\n\t\n\t// Where in the code was this error generated\n\tif (verboseLogging) {addLogEntry(\"  Calling Function:    \" ~ callingFunction, [\"verbose\"]);} // will get printed in debug\n\t\n\t// Extra Debug if we are using --verbose --verbose\n\tif (debugLogging) {\n\t\taddLogEntry(\"Raw Error Data: \" ~ message, [\"debug\"]);\n\t\taddLogEntry(\"JSON Message: \" ~ to!string(errorMessage), [\"debug\"]);\n\t}\n\t\n\t// Close out logging with an empty line, so that in console output, and logging output this becomes clear\n\taddLogEntry();\n}\n\n// Common code for handling when a client is unauthorised\nvoid handleClientUnauthorised(int httpStatusCode, JSONValue errorMessage) {\n\t// What httpStatusCode was received\n\tif (httpStatusCode == 400) {\n\t\t// bad request or a new auth token is needed\n\t\t// configure the error reason\n\t\t// Is there an error description?\n\t\tif (\"error_description\" in errorMessage) {\n\t\t\t// error_description to process\n\t\t\taddLogEntry();\n\t\t\tstring[] errorReason = splitLines(errorMessage[\"error_description\"].str);\n\t\t\taddLogEntry(to!string(errorReason[0]), [\"info\", \"notify\"]);\n\t\t\taddLogEntry();\n\t\t\taddLogEntry(\"ERROR: You will need to issue a --reauth and re-authorise this client to obtain a fresh auth token.\", [\"info\", \"notify\"]);\n\t\t\taddLogEntry();\n\t\t} else {\n\t\t\tif (\"code\" in errorMessage[\"error\"]) {\n\t\t\t\tif (errorMessage[\"error\"][\"code\"].str == \"invalidRequest\") {\n\t\t\t\t\taddLogEntry();\n\t\t\t\t\taddLogEntry(\"ERROR: Check your configuration as your existing refresh_token generated an invalid request. You may need to issue a --reauth and re-authorise this client.\", [\"info\", \"notify\"]);\n\t\t\t\t\taddLogEntry();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// no error_description\n\t\t\t\taddLogEntry();\n\t\t\t\taddLogEntry(\"ERROR: Check your configuration as it may be invalid. You will need to issue a --reauth and re-authorise this client to obtain a fresh auth token.\", [\"info\", \"notify\"]);\n\t\t\t\taddLogEntry();\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// 401 error code\n\t\taddLogEntry();\n\t\taddLogEntry(\"ERROR: Check your configuration as your refresh_token may be empty or invalid. You may need to issue a --reauth and re-authorise this client.\", [\"info\", \"notify\"]);\n\t\taddLogEntry();\n\t}\n}\n\n// Parse and display error message received from the local file system\nvoid displayFileSystemErrorMessage(string message, string callingFunction, string contextPath, FsErrorSeverity severity = FsErrorSeverity.error) {\n\t// Separate this block from surrounding log output\n\taddLogEntry();\n\n\t// Header prefix for logging accuracy\n\tstring headerPrefix = severity == FsErrorSeverity.warning ? \"WARNING\"\n\t\t\t\t\t  : severity == FsErrorSeverity.permission ? \"WARNING\"\n\t\t\t\t\t  : severity == FsErrorSeverity.fatal ? \"FATAL\"\n\t\t\t\t\t  : \"ERROR\";\n\t\n\t// Filesystem logging header\n\taddLogEntry(headerPrefix ~ \": The local file system returned an error with the following details:\");\n\t\t\n\t// Calling context (helps correlate where this came from)\n\tif (!callingFunction.empty) {\n\t\taddLogEntry(\"  Calling Function:  \" ~ callingFunction);\n\t}\n\n\t// Path context (the *thing* we were operating on)\n\tif (!contextPath.empty) {\n\t\taddLogEntry(\"  Path:              \" ~ contextPath);\n\t} else {\n\t\taddLogEntry(\"  Path:              (not available)\");\n\t}\n\n\t// Primary error message (first line) + any additional lines\n\tstring errorMessage = message;\n\tstring[] errorLines;\n\ttry {\n\t\terrorLines = splitLines(message);\n\t} catch (Exception e) {\n\t\t// splitLines should not fail, but never let logging throw\n\t\taddLogEntry(\"  NOTE: Failed to split file system exception message into lines: \" ~ e.msg);\n\t}\n\n\t// If we have lines to process\n\tif (errorLines.length > 0) {\n\t\t// First line: usually the most useful\n\t\terrorMessage = to!string(errorLines[0]);\n\t\taddLogEntry(\"  Error Message:     \" ~ errorMessage);\n\n\t\t// Remaining lines (if any) often contain errno / path / syscall details\n\t\tif (errorLines.length > 1) {\n\t\t\taddLogEntry(\"  Error Details:\");\n\t\t\tforeach (i, line; errorLines[1 .. $]) {\n\t\t\t\t// Avoid logging empty lines, but keep order\n\t\t\t\tif (!line.empty) {\n\t\t\t\t\taddLogEntry(\"     - \" ~ to!string(line));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\taddLogEntry(\"  Error Message:     No error message available\");\n\t}\n\n\t// Disk space diagnostics (best-effort) - if this is not a permission issue\n\tif (severity != FsErrorSeverity.permission) {\n\t\t// We intentionally probe both the current directory and the target path directory when possible.\n\t\ttry {\n\t\t\t// Always check the current working directory as a baseline\n\t\t\tulong freeCwd = to!ulong(getAvailableDiskSpace(\".\"));\n\t\t\taddLogEntry(\"  Disk Space (CWD):  \" ~ to!string(freeCwd) ~ \" bytes available\");\n\n\t\t\t// If we have a context path, also check its parent directory when possible.\n\t\t\t// We keep this conservative: if anything throws, just log the exception.\n\t\t\tif (!contextPath.empty) {\n\t\t\t\tstring targetProbePath = contextPath;\n\n\t\t\t\t// If it's a file path, probe the parent directory (where writes/renames happen).\n\t\t\t\t// Avoid throwing if parentDir isn't available or contextPath is weird.\n\t\t\t\ttry {\n\t\t\t\t\t// std.path.dirName handles both file/dir paths; if it returns \".\", keep as-is.\n\t\t\t\t\timport std.path : dirName;\n\t\t\t\t\tauto parent = dirName(contextPath);\n\t\t\t\t\tif (!parent.empty) targetProbePath = parent;\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\taddLogEntry(\"  NOTE: Failed to derive parent directory from path: \" ~ e.msg);\n\t\t\t\t}\n\n\t\t\t\tulong freeTarget = to!ulong(getAvailableDiskSpace(targetProbePath));\n\t\t\t\taddLogEntry(\"  Disk Space (Path): \" ~ to!string(freeTarget) ~ \" bytes available (parent path: \" ~ targetProbePath ~ \")\");\n\n\t\t\t\t// Preserve existing behaviour: if disk space check returns 0, force exit.\n\t\t\t\t// (Assumes getAvailableDiskSpace returns 0 on a hard failure in your implementation.)\n\t\t\t\tif (freeTarget == 0 || freeCwd == 0) {\n\t\t\t\t\t// Must force exit here, allow logging to be done\n\t\t\t\t\tforceExit();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Preserve existing behaviour: if disk space check returns 0, force exit.\n\t\t\t\tif (freeCwd == 0) {\n\t\t\t\t\tforceExit();\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\t// Handle exceptions from disk space check or type conversion\n\t\t\taddLogEntry(\"  NOTE: Exception during disk space check: \" ~ e.msg);\n\t\t}\n\t}\n\t\n\t// Add note for WARNING messages\n\tif (severity == FsErrorSeverity.warning) {\n\t\taddLogEntry();\n\t\taddLogEntry(\"NOTE: This warning is non-fatal; the client will continue to operate, but this may affect future operations if not resolved\");\n\t\taddLogEntry();\n\t}\n\t\n\t// Add note for filesystem permission messages\n\tif (severity == FsErrorSeverity.permission) {\n\t\taddLogEntry();\n\t\taddLogEntry(\"NOTE: Sync will continue. This file’s timestamps could not be updated because the effective user does not own the file.\");\n\t\taddLogEntry(\"      Potential Fix:\");\n\t\taddLogEntry(\"           Run the client as the file owner, or change ownership of the sync tree so it is owned by the user running the client.\");\n\t\taddLogEntry(\"      Learn more about File Ownership:\");\n\t\taddLogEntry(\"           https://www.redhat.com/en/blog/linux-file-permissions-explained\");\n\t\taddLogEntry(\"           https://unix.stackexchange.com/questions/191940/difference-between-owner-root-and-ruid-euid\");\n\t\taddLogEntry();\n\t}\n\t\n\t// Add note for ERROR messages\n\tif (severity == FsErrorSeverity.error) {\n\t\taddLogEntry();\n\t\taddLogEntry(\"NOTE: This error requires attention; the client may continue running, but functionality is impaired and the issue should be resolved.\");\n\t\taddLogEntry();\n\t}\n\t\n\t// Add note for FATAL messages\n\tif (severity == FsErrorSeverity.fatal) {\n\t\taddLogEntry();\n\t\taddLogEntry(\"NOTE: This error is fatal; the client cannot continue and this issue must be corrected before retrying. The client will now attempt to exit in a safe and orderly manner.\");\n\t\taddLogEntry();\n\t}\n}\n\n// Display the POSIX Error Message\nvoid displayPosixErrorMessage(string message) {\n\taddLogEntry(); // used rather than writeln\n\taddLogEntry(\"ERROR: Microsoft OneDrive API returned data that highlights a POSIX compliance issue:\");\n\taddLogEntry(\"  Error Message:    \" ~ message);\n}\n\n// Display the Error Message\nvoid displayGeneralErrorMessage(Exception e, string callingFunction=__FUNCTION__, int lineno=__LINE__) {\n\taddLogEntry(); // used rather than writeln\n\taddLogEntry(\"ERROR: Encountered a \" ~ e.classinfo.name ~ \":\");\n\taddLogEntry(\"  Error Message:    \" ~ e.msg);\n\taddLogEntry(\"  Calling Function: \" ~ callingFunction);\n\taddLogEntry(\"  Line number:      \" ~ to!string(lineno));\n}\n\n// Get the function name that is being called to assist with identifying where an error is being generated\nstring getFunctionName(alias func)() {\n    return __traits(identifier, __traits(parent, func)) ~ \"()\\n\";\n}\n\nJSONValue fetchOnlineURLContent(string url) {\n\t// Function variables\n\tchar[] content;\n\tJSONValue onlineContent;\n\n\t// Setup HTTP request\n\tHTTP http = HTTP();\n\t\n\t// Exit scope to ensure cleanup\n\tscope(exit) {\n\t\t// Shut http down and destroy\n\t\thttp.shutdown();\n\t\tobject.destroy(http);\n\t\t// Perform Garbage Collection\n\t\tGC.collect();\n\t\t// Return free memory to the OS\n\t\tGC.minimize();\n\t}\n\t\n\t// Configure the URL to access\n\thttp.url = url;\n\t// HTTP the connection method\n\thttp.method = HTTP.Method.get;\n\t// Data receive handler\n\thttp.onReceive = (ubyte[] data) {\n\t\tcontent ~= data; // Append data as it's received\n\t\treturn data.length;\n\t};\n\t\n\t// Perform HTTP request\n\thttp.perform();\n\t// Parse Content\n\tonlineContent = parseJSON(to!string(content));\n\t// Return onlineResponse\n    return onlineContent;\n}\n\n// Get the latest release version from GitHub\nJSONValue getLatestReleaseDetails() {\n\tJSONValue githubLatest;\n\tJSONValue versionDetails;\n\tstring latestTag;\n\tstring publishedDate;\n\t\n\t// Query GitHub for the 'latest' release details\n\ttry {\t\n\t\tgithubLatest = fetchOnlineURLContent(\"https://api.github.com/repos/abraunegg/onedrive/releases/latest\");\n    } catch (CurlException e) {\n        if (debugLogging) {addLogEntry(\"CurlException: Unable to query GitHub for latest release - \" ~ e.msg, [\"debug\"]);}\n    } catch (JSONException e) {\n        if (debugLogging) {addLogEntry(\"JSONException: Unable to parse GitHub JSON response - \" ~ e.msg, [\"debug\"]);}\n    }\n\t\n\t// githubLatest has to be a valid JSON object\n\tif (githubLatest.type() == JSONType.object){\n\t\t// use the returned tag_name\n\t\tif (\"tag_name\" in githubLatest) {\n\t\t\t// use the provided tag\n\t\t\t// \"tag_name\": \"vA.B.CC\" and strip 'v'\n\t\t\tlatestTag = strip(githubLatest[\"tag_name\"].str, \"v\");\n\t\t} else {\n\t\t\t// set to latestTag zeros\n\t\t\tif (debugLogging) {addLogEntry(\"'tag_name' unavailable in JSON response. Setting GitHub 'tag_name' release version to 0.0.0\", [\"debug\"]);}\n\t\t\tlatestTag = \"0.0.0\";\n\t\t}\n\t\t// use the returned published_at date\n\t\tif (\"published_at\" in githubLatest) {\n\t\t\t// use the provided value\n\t\t\tpublishedDate = githubLatest[\"published_at\"].str;\n\t\t} else {\n\t\t\t// set to v2.0.0 release date\n\t\t\tif (debugLogging) {addLogEntry(\"'published_at' unavailable in JSON response. Setting GitHub 'published_at' date to 2018-07-18T18:00:00Z\", [\"debug\"]);}\n\t\t\tpublishedDate = \"2018-07-18T18:00:00Z\";\n\t\t}\n\t} else {\n\t\t// JSONValue is not an object\n\t\tif (debugLogging) {addLogEntry(\"Invalid JSON Object response from GitHub. Setting GitHub 'tag_name' release version to 0.0.0\", [\"debug\"]);}\n\t\tlatestTag = \"0.0.0\";\n\t\tif (debugLogging) {addLogEntry(\"Invalid JSON Object. Setting GitHub 'published_at' date to 2018-07-18T18:00:00Z\", [\"debug\"]);}\n\t\tpublishedDate = \"2018-07-18T18:00:00Z\";\n\t}\n\t\t\n\t// return the latest github version and published date as our own JSON\n\tversionDetails = [\n\t\t\"latestTag\": JSONValue(latestTag),\n\t\t\"publishedDate\": JSONValue(publishedDate)\n\t];\n\t\n\t// return JSON\n\treturn versionDetails;\n}\n\n// Get the release details from the 'current' running version\nJSONValue getCurrentVersionDetails(string thisVersion) {\n\tJSONValue githubDetails;\n\tJSONValue versionDetails;\n\tstring versionTag = \"v\" ~ thisVersion;\n\tstring publishedDate;\n\t\n\t// Query GitHub for the release details to match the running version\n\ttry {\n\t\tgithubDetails = fetchOnlineURLContent(\"https://api.github.com/repos/abraunegg/onedrive/releases\");\n\t} catch (CurlException e) {\n        if (debugLogging) {addLogEntry(\"CurlException: Unable to query GitHub for release details - \" ~ e.msg, [\"debug\"]);}\n        return parseJSON(`{\"Error\": \"CurlException\", \"message\": \"` ~ e.msg ~ `\"}`);\n    } catch (JSONException e) {\n        if (debugLogging) {addLogEntry(\"JSONException: Unable to parse GitHub JSON response - \" ~ e.msg, [\"debug\"]);}\n        return parseJSON(`{\"Error\": \"JSONException\", \"message\": \"` ~ e.msg ~ `\"}`);\n    }\n\t\n\t// githubDetails has to be a valid JSON array\n\tif (githubDetails.type() == JSONType.array){\n\t\tforeach (searchResult; githubDetails.array) {\n\t\t\t// searchResult[\"tag_name\"].str;\n\t\t\tif (searchResult[\"tag_name\"].str == versionTag) {\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"MATCHED version\", [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"tag_name: \" ~ searchResult[\"tag_name\"].str, [\"debug\"]);\n\t\t\t\t\taddLogEntry(\"published_at: \" ~ searchResult[\"published_at\"].str, [\"debug\"]);\n\t\t\t\t}\n\t\t\t\tpublishedDate = searchResult[\"published_at\"].str;\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (publishedDate.empty) {\n\t\t\t// empty .. no version match ?\n\t\t\t// set to v2.0.0 release date\n\t\t\tif (debugLogging) {addLogEntry(\"'published_at' unavailable in JSON response. Setting GitHub 'published_at' date to 2018-07-18T18:00:00Z\", [\"debug\"]);}\n\t\t\tpublishedDate = \"2018-07-18T18:00:00Z\";\n\t\t}\n\t} else {\n\t\t// JSONValue is not an Array\n\t\tif (debugLogging) {addLogEntry(\"Invalid JSON Array. Setting GitHub 'published_at' date to 2018-07-18T18:00:00Z\", [\"debug\"]);}\n\t\tpublishedDate = \"2018-07-18T18:00:00Z\";\n\t}\n\t\t\n\t// return the latest github version and published date as our own JSON\n\tversionDetails = [\n\t\t\"versionTag\": JSONValue(thisVersion),\n\t\t\"publishedDate\": JSONValue(publishedDate)\n\t];\n\t\n\t// return JSON\n\treturn versionDetails;\n}\n\n// Check the application version versus GitHub latestTag\nvoid checkApplicationVersion() {\n\t// Get the latest details from GitHub\n\tJSONValue latestVersionDetails = getLatestReleaseDetails();\n\tstring latestVersion = latestVersionDetails[\"latestTag\"].str;\n\tSysTime publishedDate = SysTime.fromISOExtString(latestVersionDetails[\"publishedDate\"].str).toUTC();\n\tSysTime releaseGracePeriod = publishedDate;\n\tSysTime currentTime = Clock.currTime().toUTC();\n\t\n\t// drop fraction seconds\n\tpublishedDate.fracSecs = Duration.zero;\n\tcurrentTime.fracSecs = Duration.zero;\n\treleaseGracePeriod.fracSecs = Duration.zero;\n\t// roll the grace period forward to allow distributions to catch up based on their release cycles\n\treleaseGracePeriod = releaseGracePeriod.add!\"months\"(1);\n\n\t// what is this clients version?\n\tauto currentVersionArray = strip(strip(import(\"version\"), \"v\")).split(\"-\");\n\tstring applicationVersion = currentVersionArray[0];\n\t\n\t// debug output\n\tif (debugLogging) {\n\t\taddLogEntry(\"applicationVersion:       \" ~ applicationVersion, [\"debug\"]);\n\t\taddLogEntry(\"latestVersion:            \" ~ latestVersion, [\"debug\"]);\n\t\taddLogEntry(\"publishedDate:            \" ~ to!string(publishedDate), [\"debug\"]);\n\t\taddLogEntry(\"currentTime:              \" ~ to!string(currentTime), [\"debug\"]);\n\t\taddLogEntry(\"releaseGracePeriod:       \" ~ to!string(releaseGracePeriod), [\"debug\"]);\n\t}\n\t\n\t// display details if not current\n\t// is application version is older than available on GitHub\n\tif (applicationVersion != latestVersion) {\n\t\t// application version is different\n\t\tbool displayObsolete = false;\n\t\t\n\t\t// what warning do we present?\n\t\tif (applicationVersion < latestVersion) {\n\t\t\t// go get this running version details\n\t\t\tJSONValue thisVersionDetails = getCurrentVersionDetails(applicationVersion);\n\t\t\tSysTime thisVersionPublishedDate = SysTime.fromISOExtString(thisVersionDetails[\"publishedDate\"].str).toUTC();\n\t\t\tthisVersionPublishedDate.fracSecs = Duration.zero;\n\t\t\tif (debugLogging) {addLogEntry(\"thisVersionPublishedDate: \" ~ to!string(thisVersionPublishedDate), [\"debug\"]);}\n\t\t\t\n\t\t\t// the running version grace period is its release date + 1 month\n\t\t\tSysTime thisVersionReleaseGracePeriod = thisVersionPublishedDate;\n\t\t\tthisVersionReleaseGracePeriod = thisVersionReleaseGracePeriod.add!\"months\"(1);\n\t\t\tif (debugLogging) {addLogEntry(\"thisVersionReleaseGracePeriod: \" ~ to!string(thisVersionReleaseGracePeriod), [\"debug\"]);}\n\t\t\t\n\t\t\t// Is this running version obsolete ?\n\t\t\tif (!displayObsolete) {\n\t\t\t\t// if releaseGracePeriod > currentTime\n\t\t\t\t// display an information warning that there is a new release available\n\t\t\t\tif (releaseGracePeriod.toUnixTime() > currentTime.toUnixTime()) {\n\t\t\t\t\t// inside release grace period ... set flag to false\n\t\t\t\t\tdisplayObsolete = false;\n\t\t\t\t} else {\n\t\t\t\t\t// outside grace period\n\t\t\t\t\tdisplayObsolete = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// display version response\n\t\t\taddLogEntry();\n\t\t\tif (!displayObsolete) {\n\t\t\t\t// display the new version is available message\n\t\t\t\taddLogEntry(\"INFO: A new onedrive client version is available. Please upgrade your client version when possible.\", [\"info\", \"notify\"]);\n\t\t\t} else {\n\t\t\t\t// display the obsolete message\n\t\t\t\taddLogEntry(\"WARNING: Your onedrive client version is now obsolete and unsupported. Please upgrade your client version.\", [\"info\", \"notify\"]);\n\t\t\t}\n\t\t\taddLogEntry(\"Current Application Version: \" ~ applicationVersion);\n\t\t\taddLogEntry(\"Version Available:           \" ~ latestVersion);\n\t\t\taddLogEntry();\n\t\t}\n\t}\n}\n\nbool hasId(JSONValue item) {\n\treturn (\"id\" in item) != null;\n}\n\nbool hasMimeType(const ref JSONValue item) {\n\treturn (\"mimeType\" in item[\"file\"]) != null;\n}\n\nbool hasQuota(JSONValue item) {\n\treturn (\"quota\" in item) != null;\n}\n\nbool hasQuotaState(JSONValue item) {\n\treturn (\"state\" in item[\"quota\"]) != null;\n}\n\nbool isItemDeleted(JSONValue item) {\n\treturn (\"deleted\" in item) != null;\n}\n\nbool isItemRoot(JSONValue item) {\n\treturn (\"root\" in item) != null;\n}\n\nbool hasParentReference(const ref JSONValue item) {\n\treturn (\"parentReference\" in item) != null;\n}\n\nbool hasParentReferenceDriveId(JSONValue item) {\n\treturn (\"driveId\" in item[\"parentReference\"]) != null;\n}\n\nbool hasParentReferenceId(JSONValue item) {\n\treturn (\"id\" in item[\"parentReference\"]) != null;\n}\n\nbool hasParentReferencePath(JSONValue item) {\n\treturn (\"path\" in item[\"parentReference\"]) != null;\n}\n\nbool isFolderItem(const ref JSONValue item) {\n\treturn (\"folder\" in item) != null;\n}\n\nbool isRemoteFolderItem(const ref JSONValue item) {\n\tif (isItemRemote(item)) {\n\t\treturn (\"folder\" in item[\"remoteItem\"]) != null;\n\t} else {\n\t\treturn false;\n\t}\n}\n\nbool isFileItem(const ref JSONValue item) {\n\treturn (\"file\" in item) != null;\n}\n\nbool isItemRemote(const ref JSONValue item) {\n\treturn (\"remoteItem\" in item) != null;\n}\n\n// Check if [\"remoteItem\"][\"parentReference\"][\"driveId\"] exists\nbool hasRemoteParentDriveId(const ref JSONValue item) {\n    return (\"remoteItem\" in item) &&\n           (\"parentReference\" in item[\"remoteItem\"]) &&\n           (\"driveId\" in item[\"remoteItem\"][\"parentReference\"]);\n}\n\n// Check if [\"remoteItem\"][\"id\"] exists\nbool hasRemoteItemId(const ref JSONValue item) {\n    return (\"remoteItem\" in item) &&\n           (\"id\" in item[\"remoteItem\"]);\n}\n\nbool isItemFile(const ref JSONValue item) {\n\treturn (\"file\" in item) != null;\n}\n\nbool isItemFolder(const ref JSONValue item) {\n\treturn (\"folder\" in item) != null;\n}\n\nbool hasFileSize(const ref JSONValue item) {\n\treturn (\"size\" in item) != null;\n}\n\n// Function to determine if the final component of the provided path is a .file or .folder\nbool isDotFile(const(string) path) {\n    // Check for null or empty path\n    if (path is null || path.length == 0) {\n        return false;\n    }\n\n    // Special case for root\n    if (path == \".\") {\n        return false;\n    }\n\n    // Extract the last component of the path\n    auto paths = pathSplitter(buildNormalizedPath(path));\n    \n    // Optimised way to fetch the last component\n    string lastComponent = paths.empty ? \"\" : paths.back;\n\n    // Check if the last component starts with a dot\n    return startsWith(lastComponent, \".\");\n}\n\nbool isMalware(const ref JSONValue item) {\n\treturn (\"malware\" in item) != null;\n}\n\nbool isOneNotePackageFolder(const ref JSONValue item) {\n    if (\"package\" in item) {\n        auto pkg = item[\"package\"];\n        if (\"type\" in pkg && pkg[\"type\"].type == JSONType.string) {\n            return pkg[\"type\"].str == \"oneNote\";\n        }\n    }\n    return false;\n}\n\nbool hasHashes(const ref JSONValue item) {\n\treturn (\"hashes\" in item[\"file\"]) != null;\n}\n\nbool hasZeroHashes(const ref JSONValue item) {\n    // Check if \"hashes\" exists under \"file\" and is empty\n    if (\"hashes\" in item[\"file\"]) {\n        auto hashes = item[\"file\"][\"hashes\"];\n        if (hashes.type == JSONType.object && hashes.object.keys.length == 0) {\n            return true;\n        }\n    }\n    return false;\n}\n\nbool hasQuickXorHash(const ref JSONValue item) {\n\treturn (\"quickXorHash\" in item[\"file\"][\"hashes\"]) != null;\n}\n\nbool hasSHA256Hash(const ref JSONValue item) {\n\treturn (\"sha256Hash\" in item[\"file\"][\"hashes\"]) != null;\n}\n\nbool isMicrosoftOneNoteMimeType1(const ref JSONValue item) {\n\treturn (item[\"file\"][\"mimeType\"].str) == \"application/msonenote\";\n}\n\nbool isMicrosoftOneNoteMimeType2(const ref JSONValue item) {\n\treturn (item[\"file\"][\"mimeType\"].str) == \"application/octet-stream\";\n}\n\nbool isMicrosoftOneNoteFileExtensionType1(const ref JSONValue item) {\n    return item[\"name\"].str.endsWith(\".one\");\n}\n\nbool isMicrosoftOneNoteFileExtensionType2(const ref JSONValue item) {\n    return item[\"name\"].str.endsWith(\".onetoc2\");\n}\n\nbool hasUploadURL(const ref JSONValue item) {\n\treturn (\"uploadUrl\" in item) != null;\n}\n\nbool hasNextExpectedRanges(const ref JSONValue item) {\n\treturn (\"nextExpectedRanges\" in item) != null;\n}\n\nbool hasLocalPath(const ref JSONValue item) {\n\treturn (\"localPath\" in item) != null;\n}\n\nbool hasETag(const ref JSONValue item) {\n\treturn (\"eTag\" in item) != null;\n}\n\nbool hasSharedElement(const ref JSONValue item) {\n\treturn (\"shared\" in item) != null;\n}\n\nbool hasName(const ref JSONValue item) {\n\treturn (\"name\" in item) != null;\n}\n\nbool hasCreatedBy(const ref JSONValue item) {\n\treturn (\"createdBy\" in item) != null;\n}\n\nbool hasCreatedByUser(const ref JSONValue item) {\n\treturn (\"user\" in item[\"createdBy\"]) != null;\n}\n\nbool hasCreatedByUserDisplayName(const ref JSONValue item) {\n\tif (hasCreatedBy(item)) {\n\t\tif (hasCreatedByUser(item)) {\n\t\t\treturn (\"displayName\" in item[\"createdBy\"][\"user\"]) != null;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t} else {\n\t\treturn false;\n\t}\n}\n\nbool hasLastModifiedBy(const ref JSONValue item) {\n\treturn (\"lastModifiedBy\" in item) != null;\n}\n\nbool hasLastModifiedByUser(const ref JSONValue item) {\n\treturn (\"user\" in item[\"lastModifiedBy\"]) != null;\n}\n\nbool hasLastModifiedByUserDisplayName(const ref JSONValue item) {\n\tif (hasLastModifiedBy(item)) {\n\t\tif (hasLastModifiedByUser(item)) {\n\t\t\treturn (\"displayName\" in item[\"lastModifiedBy\"][\"user\"]) != null;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t} else {\n\t\treturn false;\n\t}\n}\n\n// Check Intune JSON response for 'accessToken'\nbool hasAccessTokenData(const ref JSONValue item) {\n\treturn (\"accessToken\" in item) != null;\n}\n\n// Check Intune JSON response for 'account'\nbool hasAccountData(const ref JSONValue item) {\n\treturn (\"account\" in item) != null;\n}\n\n// Check Intune JSON response for 'expiresOn'\nbool hasExpiresOn(const ref JSONValue item) {\n\treturn (\"expiresOn\" in item) != null;\n}\n\n// Resumable Download checks\nbool hasDriveId(const ref JSONValue item) {\n\treturn (\"driveId\" in item) != null;\n}\n\nbool hasItemId(const ref JSONValue item) {\n\treturn (\"itemId\" in item) != null;\n}\n\nbool hasDownloadFilename(const ref JSONValue item) {\n\treturn (\"downloadFilename\" in item) != null;\n}\n\nbool hasResumeOffset(const ref JSONValue item) {\n\treturn (\"resumeOffset\" in item) != null;\n}\n\nbool hasOnlineHash(const ref JSONValue item) {\n\treturn (\"onlineHash\" in item) != null;\n}\n\nbool hasQuickXorHashResume(const ref JSONValue item) {\n\treturn (\"quickXorHash\" in item[\"onlineHash\"]) != null;\n}\n\nbool hasSHA256HashResume(const ref JSONValue item) {\n\treturn (\"sha256Hash\" in item[\"onlineHash\"]) != null;\n}\n\n// Test if a path is the equivalent of root '.'\nbool isRootEquivalent(string inputPath) {\n\tauto normalisedPath = buildNormalizedPath(inputPath);\n\treturn normalisedPath == \".\" || normalisedPath == \"\";\n}\n\n// Convert bytes to GB\nstring byteToGibiByte(ulong bytes) {\n    if (bytes == 0) {\n        return \"0.00\"; // or handle the zero case as needed\n    }\n\n    double gib = bytes / 1073741824.0; // 1024^3 for direct conversion\n    return format(\"%.2f\", gib); // Format to ensure two decimal places\n}\n\n// Test if entrypoint.sh exists on the root filesystem\nbool entrypointExists(string basePath = \"/\") {\n    try {\n        // Build the path to the entrypoint.sh file\n        string entrypointPath = buildNormalizedPath(buildPath(basePath, \"entrypoint.sh\"));\n\n        // Check if the path exists and return the result\n        return exists(entrypointPath);\n    } catch (Exception e) {\n        // Handle any exceptions (e.g., permission issues, invalid path)\n        addLogEntry(\"An error occurred: \" ~ e.msg);\n        return false;\n    }\n}\n\n// Generate a random alphanumeric string with specified length\nstring generateAlphanumericString(size_t length = 16) {\n    // Ensure length is not zero\n    if (length == 0) {\n        throw new Exception(\"Length must be greater than 0\");\n    }\n\n    auto asciiLetters = to!(dchar[])(letters);\n    auto asciiDigits = to!(dchar[])(digits);\n    dchar[] randomString;\n    randomString.length = length;\n\n    // Create a random number generator\n    auto rndGen = Random(unpredictableSeed);\n\n    // Fill the string with random alphanumeric characters\n    fill(randomString[], randomCover(chain(asciiLetters, asciiDigits), rndGen));\n\n    return to!string(randomString);\n}\n\n// Display internal memory stats pre garbage collection\nvoid displayMemoryUsagePreGC() {\n\t// Display memory usage\n\taddLogEntry();\n\taddLogEntry(\"Memory Usage PRE Garbage Collection (KB)\");\n\taddLogEntry(\"-----------------------------------------------------\");\n\twriteMemoryStats();\n\taddLogEntry();\n}\n\n// Display internal memory stats post garbage collection + RSS (actual memory being used)\nvoid displayMemoryUsagePostGC() {\n    // Display memory usage title\n    addLogEntry(\"Memory Usage POST Garbage Collection (KB)\");\n    addLogEntry(\"-----------------------------------------------------\");\n    writeMemoryStats();  // Assuming this function logs memory stats correctly\n\n    // Query the actual Resident Set Size (RSS) for the PID\n    pid_t pid = getCurrentPID();\n    ulong rss = getRSS(pid);\n\n    // Check and log the previous RSS value\n    if (previousRSS != 0) {\n        addLogEntry(\"previous Resident Set Size (RSS)         = \" ~ to!string(previousRSS) ~ \" KB\");\n        \n        // Calculate and log the difference in RSS\n        long difference = rss - previousRSS;  // 'difference' can be negative, use 'long' to handle it\n        string sign = difference > 0 ? \"+\" : (difference < 0 ? \"\" : \"\");  // Determine the sign for display, no sign for zero\n        addLogEntry(\"difference in Resident Set Size (RSS)    = \" ~ sign ~ to!string(difference) ~ \" KB\");\n    }\n    \n    // Update previous RSS with the new value\n    previousRSS = rss;\n    \n    // Closeout\n\taddLogEntry();\n}\n\n// Write internal memory stats\nvoid writeMemoryStats() {\n\taddLogEntry(\"current memory usedSize                  = \" ~ to!string((GC.stats.usedSize/1024))); // number of used bytes on the GC heap (might only get updated after a collection)\n\taddLogEntry(\"current memory freeSize                  = \" ~ to!string((GC.stats.freeSize/1024))); // number of free bytes on the GC heap (might only get updated after a collection)\n\taddLogEntry(\"current memory allocatedInCurrentThread  = \" ~ to!string((GC.stats.allocatedInCurrentThread/1024))); // number of bytes allocated for current thread since program start\n\t\n\t// Query the actual Resident Set Size (RSS) for the PID\n\tpid_t pid = getCurrentPID();\n\tulong rss = getRSS(pid);\n\t// The RSS includes all memory that is currently marked as occupied by the process. \n\t// 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.\n\t// This includes memory that might not be actively used but has not been returned to the system. \n\t// The GC.minimize() function can sometimes cause an increase in RSS due to how memory pages are managed and freed.\n\taddLogEntry(\"current Resident Set Size (RSS)          = \" ~ to!string(rss)  ~ \" KB\"); // actual memory in RAM used by the process at this point in time\n}\n\n// Return the username of the UID running the 'onedrive' process\nstring getUserName() {\n    // Retrieve the UID of the current user\n    auto uid = getuid();\n\n    // Retrieve password file entry for the user\n    auto pw = getpwuid(uid);\n\t\n\t// If user info is not found (e.g. no /etc/passwd entry), fallback to environment\n    if (pw is null) {\n        if (debugLogging) {\n            addLogEntry(\"Unable to retrieve user info for UID: \" ~ to!string(uid), [\"debug\"]);\n            addLogEntry(\"Falling back to environment variable USER or returning 'unknown'\", [\"debug\"]);\n        }\n\n        // Try environment variable\n        string userEnv = environment.get(\"USER\", \"unknown\");\n        return userEnv.length > 0 ? userEnv : \"unknown\";\n    }\n\t\n\t// If pw is valid, we can safely access pw.pw_name\n    string userName = to!string(fromStringz(pw.pw_name));\n\n    // Log User identifiers from process\n\tif (debugLogging) {\n\t\taddLogEntry(\"Process ID: \" ~ to!string(pw), [\"debug\"]);\n\t\taddLogEntry(\"User UID:   \" ~ to!string(pw.pw_uid), [\"debug\"]);\n\t\taddLogEntry(\"User GID:   \" ~ to!string(pw.pw_gid), [\"debug\"]);\n\t}\n\n    // Check if username is valid\n    if (!userName.empty) {\n        if (debugLogging) {addLogEntry(\"User Name:  \" ~ userName, [\"debug\"]);}\n        return userName;\n    } else {\n        // Log and return unknown user\n        if (debugLogging) {addLogEntry(\"User Name:  unknown\", [\"debug\"]);}\n        return \"unknown\";\n    }\n}\n\n// Get resource limit in POSIX portable manner (soft limit max open files)\nulong getSoftOpenFilesLimit() {\n    rlimit lim;\n    if (getrlimit(RLIMIT_NOFILE, &lim) == 0)\n        return cast(ulong) lim.rlim_cur; // soft limit\n    return 0;\n}\n\n// Get resource limit in POSIX portable manner (hard limit max open files)\nulong getHardOpenFilesLimit() {\n    rlimit lim;\n    if (getrlimit(RLIMIT_NOFILE, &lim) == 0)\n        return cast(ulong) lim.rlim_max; // hard limit\n    return 0; // or throw / handle error\n}\n\n// Calculate the ETA for when a 'large file' will be completed (upload & download operations)\nint calc_eta(size_t counter, size_t iterations, long start_time) {\n\tif (counter == 0) {\n\t\treturn 0; // Avoid division by zero\n\t}\n\t\n\t// Get the current time as a Unix timestamp (seconds since the epoch, January 1, 1970, 00:00:00 UTC)\n\tSysTime currentTime = Clock.currTime();\n\tlong current_time = currentTime.toUnixTime();\n\n\t// 'start_time' must be less than 'current_time' otherwise ETA will have negative values\n\tif (start_time > current_time) {\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Warning: start_time is in the future. Cannot calculate ETA.\", [\"debug\"]);\n\t\t}\n\t\treturn 0;\n\t}\n\t\n\t// Calculate duration\n\tlong duration = (current_time - start_time);\n\n\t// Calculate the ratio we are at\n\tdouble ratio = cast(double) counter / iterations;\n\n\t// Calculate segments left to download\n\tauto segments_remaining = (iterations > counter) ? (iterations - counter) : 0;\n\n\t// Calculate the average time per iteration so far\n\tdouble avg_time_per_iteration = cast(double) duration / counter;\n\n\t// Debug output for the ETA calculation\n\tif (debugLogging) {\n\t\taddLogEntry(\"counter:                \" ~ to!string(counter), [\"debug\"]);\n\t\taddLogEntry(\"iterations:             \" ~ to!string(iterations), [\"debug\"]);\n\t\taddLogEntry(\"segments_remaining:     \" ~ to!string(segments_remaining), [\"debug\"]);\n\t\taddLogEntry(\"ratio:                  \" ~ format(\"%.2f\", ratio), [\"debug\"]);\n\t\taddLogEntry(\"start_time:             \" ~ to!string(start_time), [\"debug\"]);\n\t\taddLogEntry(\"current_time:           \" ~ to!string(current_time), [\"debug\"]);\n\t\taddLogEntry(\"duration:               \" ~ to!string(duration), [\"debug\"]);\n\t\taddLogEntry(\"avg_time_per_iteration: \" ~ format(\"%.2f\", avg_time_per_iteration), [\"debug\"]);\n\t}\n\t\n\t// Return the ETA or duration\n    if (counter != iterations) {\n\t\tauto eta_sec = avg_time_per_iteration * segments_remaining;\n\t\t// ETA Debug\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"eta_sec:                \" ~ to!string(eta_sec), [\"debug\"]);\n\t\t\taddLogEntry(\"estimated_total_time:   \" ~ to!string(avg_time_per_iteration * iterations), [\"debug\"]);\n\t\t}\n\t\t// Return ETA\n\t\treturn eta_sec > 0 ? cast(int) ceil(eta_sec) : 0;\n\t} else {\n\t\t// Return the average time per iteration for the last iteration\n\t\treturn cast(int) ceil(avg_time_per_iteration); \n    }\n}\n\n// Use the ETA value and return a formatted string in a consistent manner\nstring formatETA(int eta) {\n\t// How do we format the ETA string. Guard against zero and negative values\n\tif (eta <= 0) {\n\t\treturn \"|  ETA    --:--:--\";\n\t}\n\tint h, m, s;\n\tdur!\"seconds\"(eta).split!(\"hours\", \"minutes\", \"seconds\")(h, m, s);\n\treturn format!\"|  ETA    %02d:%02d:%02d\"(h, m, s);\n}\n\n// Force Exit due to failure\nvoid forceExit() {\n\t// Allow any logging complete before we force exit\n\tThread.sleep(dur!(\"msecs\")(500));\n\t// Shutdown logging, which also flushes all logging buffers\n\tshutdownLogging();\n\t// Setup signal handling for the exit scope\n\tsetupExitScopeSignalHandler();\n\t// Force Exit\n\texit(EXIT_FAILURE);\n}\n\n// Get the current PID of the application\npid_t getCurrentPID() {\n    // The '/proc/self' is a symlink to the current process's proc directory\n    string path = \"/proc/self/stat\";\n    \n    // Read the content of the stat file\n    string content;\n    try {\n        content = readText(path);\n    } catch (Exception e) {\n        writeln(\"Failed to read stat file: \", e.msg);\n        return 0;\n    }\n\n    // The first value in the stat file is the PID\n    auto parts = split(content);\n    return to!pid_t(parts[0]);  // Convert the first part to pid_t\n}\n\n// Access the Resident Set Size (RSS) based on the PID of the running application\nulong getRSS(pid_t pid) {\n    // Construct the path to the statm file for the given PID\n    string path = format(\"/proc/%s/statm\", to!string(pid));\n\n    // Read the content of the file\n    string content;\n    try {\n        content = readText(path);\n    } catch (Exception e) {\n        writeln(\"Failed to read statm file: \", e.msg);\n        return 0;\n    }\n\n    // Split the content and get the RSS (second value)\n    auto stats = split(content);\n    if (stats.length < 2) {\n        writeln(\"Unexpected format in statm file.\");\n        return 0;\n    }\n\n    // RSS is in pages, convert it to kilobytes\n    ulong rssPages = to!ulong(stats[1]);\n    ulong rssKilobytes = rssPages * sysconf(_SC_PAGESIZE) / 1024;\n    return rssKilobytes;\n}\n\n// Getting around the @nogc problem\n// https://p0nce.github.io/d-idioms/#Bypassing-@nogc\nauto assumeNoGC(T) (T t) if (isFunctionPointer!T || isDelegate!T) {\n\tenum attrs = functionAttributes!T | FunctionAttribute.nogc;\n\treturn cast(SetFunctionAttributes!(T, functionLinkage!T, attrs)) t;\n}\n\n// When using exit scopes, set up this to catch any undesirable signal\nvoid setupExitScopeSignalHandler() {\n\tsigaction_t action;\n\taction.sa_handler = &exitScopeSignalHandler; // Direct function pointer assignment\n\tsigemptyset(&action.sa_mask); // Initialize the signal set to empty\n\taction.sa_flags = 0;\n\tsigaction(SIGSEGV, &action, null); // Invalid Memory Access signal\n}\n\n// Catch any SIGSEV generated by the exit scopes\nextern(C) nothrow @nogc @system void exitScopeSignalHandler(int signo) {\n\tif (signo == SIGSEGV) {\n\t\tassumeNoGC ( () {\n\t\t\t// Caught a SIGSEGV but everything was shutdown cleanly .....\n\t\t\t//printf(\"Caught a SIGSEGV but everything was shutdown cleanly .....\\n\");\n\t\t\texit(0);\n\t\t})();\n\t}\n}\n\n// Return the compiler details\nstring compilerDetails() {\n\tversion(DigitalMars) enum compiler = \"DMD\";\n\telse version(LDC)    enum compiler = \"LDC\";\n\telse version(GNU)    enum compiler = \"GDC\";\n\telse enum compiler = \"Unknown compiler\";\n\tstring compilerString = compiler ~ \" \" ~ to!string(__VERSION__);\n\treturn compilerString;\n}\n\n// Return the curl version details\nstring getCurlVersionString() {\n\t// Get curl version\n\tauto versionInfo = curl_version();\n\treturn to!string(versionInfo);\n}\n\n// Function to return the decoded curl version as a string\nstring getCurlVersionNumeric() {\n    // Get curl version info using curl_version_info\n    auto curlVersionDetails = curl_version_info(CURLVERSION_NOW);\n\n    // Extract the major, minor, and patch numbers from version_num\n    uint versionNum = curlVersionDetails.version_num;\n    \n    // The version number is in the format 0xXXYYZZ\n    uint major = (versionNum >> 16) & 0xFF; // Extract XX (major version)\n    uint minor = (versionNum >> 8) & 0xFF;  // Extract YY (minor version)\n    uint patch = versionNum & 0xFF;         // Extract ZZ (patch version)\n\n    // Return the version in the format \"major.minor.patch\"\n    return major.to!string ~ \".\" ~ minor.to!string ~ \".\" ~ patch.to!string;\n}\n\n// Test the curl version against known curl versions with HTTP/2 issues\nbool isBadCurlVersion(string curlVersion) {\n    // List of known curl versions with HTTP/2 issues\n    string[] supportedVersions = [\n\t\t\"7.68.0\", // Ubuntu 20.x\n\t\t\"7.74.0\", // Debian 11\n\t\t\"7.81.0\", // Ubuntu 22.x\n\t\t\"7.88.1\", // Debian 12\n\t\t\"8.2.1\",  // Ubuntu 23.10\n\t\t\"8.5.0\",  // Ubuntu 24.04\n\t\t\"8.9.1\",  // Ubuntu 24.10\n\t\t\"8.10.0\",  // Various - HTTP/2 bug which was fixed in 8.10.1\n\t\t\"8.13.0\",  // Has a SSL Certificate read issue fixed by 8.14.1\n\t\t\"8.13.1\",  // Has a SSL Certificate read issue fixed by 8.14.1\n\t\t\"8.14.0\",  // Has a SSL Certificate read issue fixed by 8.14.1\n    ];\n    \n    // Check if the current version matches one of the supported versions\n    return canFind(supportedVersions, curlVersion);\n}\n\n// Is the operation a transient error?\nprivate bool isTransientErrno(int err) @safe nothrow {\n\t// EINTR: interrupted system call\n\t// EBUSY: resource busy (can be transient on some FS / mount scenarios)\n\t// EAGAIN: try again (transient)\n\treturn err == EINTR || err == EBUSY || err == EAGAIN;\n}\n\n// Retry wrapper for getTimes()\nprivate bool safeGetTimes(string path, out SysTime accessTime, out SysTime modTime, string thisFunctionName) {\n\tint maxAttempts = 5;\n\n\tforeach (attempt; 0 .. maxAttempts) {\n\t\ttry {\n\t\t\tgetTimes(path, accessTime, modTime);\n\t\t\treturn true;\n\t\t} catch (FileException e) {\n\t\t\t// If path vanished between checks / operations, treat as non-fatal for this workflow\n\t\t\tif (e.errno == ENOENT) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif (isTransientErrno(e.errno)) {\n\t\t\t\t// bounded backoff to avoid spinning\n\t\t\t\tThread.sleep(dur!\"msecs\"(10 * (attempt + 1)));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, path);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tdisplayFileSystemErrorMessage(\"Failed to read file timestamps after retries\", thisFunctionName, path);\n\treturn false;\n}\n\n// Some errnos are 'expected' in the wild (permissions, RO mounts, immutable files)\n// What is this errno\nprivate bool isExpectedPermissionStyleErrno(int err) {\n    // Return true of this is an expected error due to permission issues\n    return err == EPERM || err == EACCES || err == EROFS;\n}\n\n// Helper function to determine path mismatch against UID|GID and process effective UID\nprivate bool getPathOwnerMismatch(string path, out uint fileUid, out uint effectiveUid) {\n\tversion (Posix) {\n\t\tstat_t st;\n\n\t\t// Default outputs\n\t\tfileUid = 0;\n\t\teffectiveUid = cast(uint) geteuid();\n\n\t\ttry {\n\t\t\t// absolutePath can throw; keep this helper non-throwing\n\t\t\tauto fullPath = absolutePath(path);\n\n\t\t\t// Ensure we pass a NUL-terminated string to the C API\n\t\t\tauto cpath = toStringz(fullPath);\n\n\t\t\tif (lstat(cpath, &st) != 0) {\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\taddLogEntry(\"getPathOwnerMismatch(): lstat() failed for '\" ~ path ~ \"'\", [\"debug\"]);\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tfileUid = cast(uint) st.st_uid;\n\t\t\t// effectiveUid already set above\n\t\t\treturn fileUid != effectiveUid;\n\n\t\t} catch (Exception e) {\n\t\t\tif (debugLogging) {\n\t\t\t\taddLogEntry(\"getPathOwnerMismatch(): exception for '\" ~ path ~ \"': \" ~ e.msg, [\"debug\"]);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t} else {\n\t\tfileUid = 0;\n\t\teffectiveUid = 0;\n\t\treturn false;\n\t}\n}\n\n// Retry wrapper for setTimes()\nprivate bool safeSetTimes(string path, SysTime accessTime, SysTime modTime, string thisFunctionName) {\n\t\n\tenum int maxAttempts = 5;\n\n\tforeach (attempt; 0 .. maxAttempts) {\n\t\ttry {\n\t\t\tsetTimes(path, accessTime, modTime);\n\t\t\treturn true;\n\t\t} catch (FileException e) {\n\t\t\t// If the path disappeared before we could set, there's nothing useful to do\n\t\t\tif (e.errno == ENOENT) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Transient filesystem error: retry with backoff\n\t\t\tif (isTransientErrno(e.errno)) {\n\t\t\t\tif (debugLogging) {\n\t\t\t\t\t// Log that we hit a transient error when doing debugging, otherwise nothing\n\t\t\t\t\taddLogEntry(\"safeSetTimes() transient filesystem error response: \" ~ e.msg ~ \"\\n - Attempting retry for setTimes()\", [\"debug\"]);\n\t\t\t\t}\n\t\t\t\t// Backoff and retry\n\t\t\t\tThread.sleep(dur!\"msecs\"(15 * (attempt + 1)));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Non-transient: special-case common permission errors\n\t\t\t// The user running the client needs to be the owner of the files if the client needs to set explicit timestamps\n\t\t\t// See https://github.com/abraunegg/onedrive/issues/3651 for details\n\t\t\tif (isExpectedPermissionStyleErrno(e.errno)) {\n\t\t\t\t// Configure application message to display\n\t\t\t\tstring permissionErrorMessage = \"Unable to set local file timestamps (mtime/atime): Operation not permitted\";\n\t\t\t\tif (e.errno == EPERM) {\n\t\t\t\t\tpermissionErrorMessage = permissionErrorMessage ~ \" (EPERM)\";\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (e.errno == EACCES) {\n\t\t\t\t\tpermissionErrorMessage = permissionErrorMessage ~ \" (EACCES)\";\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (e.errno == EROFS) {\n\t\t\t\t\tpermissionErrorMessage = permissionErrorMessage ~ \" (EROFS)\";\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Get extra details if required\n\t\t\t\tstring extraHint;\n\t\t\t\tuint fileUid;\n\t\t\t\tuint effectiveUid;\n\t\t\t\t\n\t\t\t\tif (e.errno == EPERM && getPathOwnerMismatch(path, fileUid, effectiveUid)) {\n\t\t\t\t\textraHint =\n\t\t\t\t\t\t\"\\nThe onedrive client user does not own this file. onedrive user effective UID=\" ~ to!string(effectiveUid) ~ \", file owner UID=\" ~ to!string(fileUid) ~ \".\" ~\n\t\t\t\t\t\t\"\\nOn Unix-like systems, setting explicit file timestamps typically requires the process to be the file owner or run with sufficient privileges.\";\n\t\t\t\t\t\n\t\t\t\t\t// Update permissionErrorMessage to add extraHint\n\t\t\t\t\tpermissionErrorMessage = permissionErrorMessage ~ extraHint;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// If we are doing --verbose or --debug display this file system error\n\t\t\t\tif (verboseLogging) {\n\t\t\t\t\t// Display applicable message for the user regarding permission error on path\n\t\t\t\t\tdisplayFileSystemErrorMessage(\n\t\t\t\t\t\tpermissionErrorMessage,\n\t\t\t\t\t\tthisFunctionName,\n\t\t\t\t\t\tpath,\n\t\t\t\t\t\tFsErrorSeverity.permission\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// It is pointless attempting a re-try in this scenario as those conditions will not change by retrying 15ms later.\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Everything else: preserve existing behaviour\n\t\t\tdisplayFileSystemErrorMessage(e.msg, thisFunctionName, path);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Only reached if transient errors never resolved\n\tdisplayFileSystemErrorMessage(\"Failed to set path timestamps after retries\", thisFunctionName, path);\n\treturn false;\n}\n\n// Set the timestamp of the provided path to ensure this is done in a consistent manner\nvoid setLocalPathTimestamp(bool dryRun, string inputPath, SysTime newTimeStamp) {\n\t// Set this function name\n\tstring thisFunctionName = format(\"%s.%s\", strip(__MODULE__), strip(getFunctionName!({})));\n\n\tif (dryRun) {\n\t\t// Keep behaviour consistent: do nothing in dry-run\n\t\treturn;\n\t}\n\n\tif (debugLogging) {\n\t\tstring logMessage = format(\"Setting 'lastAccessTime' and 'lastModificationTime' properties for: %s to %s if required\", inputPath, to!string(newTimeStamp));\n\t\taddLogEntry(logMessage, [\"debug\"]);\n\t}\n\n\t// Read existing times (with retry protection)\n\tSysTime existingAccessTime;\n\tSysTime existingModificationTime;\n\n\tif (!safeGetTimes(inputPath, existingAccessTime, existingModificationTime, thisFunctionName)) {\n\t\t// safeGetTimes already logged non-transient errors; ENOENT etc just returns false quietly\n\t\treturn;\n\t}\n\n\tif (debugLogging) {\n\t\taddLogEntry(\"Existing timestamp values:\", [\"debug\"]);\n\t\taddLogEntry(\"  Access Time:       \" ~ to!string(existingAccessTime), [\"debug\"]);\n\t\taddLogEntry(\"  Modification Time: \" ~ to!string(existingModificationTime), [\"debug\"]);\n\t}\n\n\t// Compare timestamps using UTC and truncated fractional seconds (OneDrive has no fractional seconds)\n\tSysTime newTimeStampZeroFracSec = newTimeStamp.toUTC();\n\tSysTime existingTimeStampZeroFracSec = existingModificationTime.toUTC();\n\n\tnewTimeStampZeroFracSec.fracSecs = Duration.zero;\n\texistingTimeStampZeroFracSec.fracSecs = Duration.zero;\n\n\tif (debugLogging) {\n\t\taddLogEntry(\"Comparison timestamp values:\", [\"debug\"]);\n\t\taddLogEntry(\"  newTimeStampZeroFracSec =      \" ~ to!string(newTimeStampZeroFracSec), [\"debug\"]);\n\t\taddLogEntry(\"  existingTimeStampZeroFracSec = \" ~ to!string(existingTimeStampZeroFracSec), [\"debug\"]);\n\t}\n\n\t// Only update if the whole-second timestamp differs\n\tbool makeTimestampChange = (newTimeStampZeroFracSec != existingTimeStampZeroFracSec);\n\n\tSysTime updatedModificationTime;\n\tif (!makeTimestampChange) {\n\t\tif (debugLogging) {\n\t\t\taddLogEntry(\"Fractional seconds only difference in modification time; preserving existing modification time\", [\"debug\"]);\n\t\t\taddLogEntry(\"No local timestamp change required\", [\"debug\"]);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (debugLogging) {\n\t\taddLogEntry(\"New timestamp is different to existing timestamp; using new modification time\", [\"debug\"]);\n\t\taddLogEntry(\"Calling setTimes() for the given path\", [\"debug\"]);\n\t}\n\n\tupdatedModificationTime = newTimeStamp;\n\n\t// Apply new timestamp\n\tif (!safeSetTimes(inputPath, existingAccessTime, updatedModificationTime, thisFunctionName)) {\n\t\t// safeSetTimes logs non-transient errors; ENOENT just returns false quietly\n\t\treturn;\n\t}\n\n\tif (debugLogging) {\n\t\taddLogEntry(\"Timestamp updated for this path: \" ~ inputPath, [\"debug\"]);\n\t}\n\t\n\t// Post-check to ensure timestamp is set\n\tSysTime newAccessTime;\n\tSysTime newModificationTime;\n\tif (safeGetTimes(inputPath, newAccessTime, newModificationTime, thisFunctionName) && debugLogging) {\n\t\taddLogEntry(\"Current timestamp values post any change (if required):\", [\"debug\"]);\n\t\taddLogEntry(\"  Access Time:       \" ~ to!string(newAccessTime), [\"debug\"]);\n\t\taddLogEntry(\"  Modification Time: \" ~ to!string(newModificationTime), [\"debug\"]);\n\t}\n}\n\n// Generate the initial function processing time log entry\nvoid displayFunctionProcessingStart(string functionName, string logKey) {\n\t// Output the function processing header\n\taddLogEntry(format(\"[%s] Application Function '%s' Started\", strip(logKey), strip(functionName)));\n}\n\n// Calculate the time taken to perform the application Function\nvoid displayFunctionProcessingTime(string functionName, SysTime functionStartTime, SysTime functionEndTime, string logKey) {\n\t// Calculate processing time\n\tauto functionDuration = functionEndTime - functionStartTime;\n\tdouble functionDurationAsSeconds = (functionDuration.total!\"msecs\"/1e3); // msec --> seconds\n\t\n\t// Output the function processing time\n\tstring processingTime = format(\"[%s] Application Function '%s' Processing Time = %.4f Seconds\", strip(logKey), strip(functionName), functionDurationAsSeconds);\n\taddLogEntry(processingTime);\n}\n\n// Return true if `dir` exists and has no entries.\n// Symlinks are treated as non-removable.\nbool isDirEmpty(string dir) {\n    if (!exists(dir) || !isDir(dir) || isSymlink(dir)) return false;\n    foreach (_; dirEntries(dir, SpanMode.shallow)) {\n        // Found at least one entry\n        return false;\n    }\n    return true;\n}\n\n// Escape a string for literal use inside a regex\nstring regexEscape(string s) {\n\tauto b = appender!string();\n\tforeach (c; s) {\n\t\t// characters with special meaning in regex\n\t\timmutable specials = \"\\\\.^$|?*+()[]{}\";\n\t\tif (specials.canFind(c)) b.put('\\\\');\n\t\tb.put(c);\n\t}\n\treturn b.data;\n}\n\n// Update lastLocalWrite to denote we just performed a local-originated write\nvoid markLocalWrite() {\n    lastLocalWrite = MonoTime.currTime();\n}\n"
  },
  {
    "path": "src/webhook.d",
    "content": "// What is this module called?\nmodule webhook;\n\n// What does this module require to function?\nimport core.atomic : atomicOp;\nimport std.datetime;\nimport std.concurrency;\nimport std.json;\n\n// What other modules that we have created do we need to import?\nimport arsd.cgi;\nimport config;\nimport onedrive;\nimport log;\nimport util;\n\nclass OneDriveWebhook {\n\tprivate RequestServer server;\n\tprivate string host;\n\tprivate ushort port;\n\tprivate Tid parentTid;\n\tprivate bool started;\n\n    private ApplicationConfig appConfig;\n\tprivate OneDriveApi oneDriveApiInstance;\n\tstring subscriptionId = \"\";\n\tSysTime subscriptionExpiration, subscriptionLastErrorAt;\n\tDuration subscriptionExpirationInterval, subscriptionRenewalInterval, subscriptionRetryInterval;\n\tstring notificationUrl = \"\";\n\n\tprivate uint count;\n\t\n    this(Tid parentTid, ApplicationConfig appConfig) {\n\t\tthis.host = appConfig.getValueString(\"webhook_listening_host\");\n\t\tthis.port = to!ushort(appConfig.getValueLong(\"webhook_listening_port\"));\n\t\tthis.parentTid = parentTid;\n        this.appConfig = appConfig;\n\n\t\tsubscriptionExpiration = Clock.currTime(UTC());\n\t\tsubscriptionLastErrorAt = SysTime.fromUnixTime(0);\n\t\tsubscriptionExpirationInterval = dur!\"seconds\"(appConfig.getValueLong(\"webhook_expiration_interval\"));\n\t\tsubscriptionRenewalInterval = dur!\"seconds\"(appConfig.getValueLong(\"webhook_renewal_interval\"));\n\t\tsubscriptionRetryInterval = dur!\"seconds\"(appConfig.getValueLong(\"webhook_retry_interval\"));\n\t\tnotificationUrl = appConfig.getValueString(\"webhook_public_url\");\n\t}\n\t\n\t// The static serve() is necessary because spawn() does not like instance methods\n\tvoid serve() {\n\t\tif (this.started) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tthis.started = true;\n\t\tthis.count = 0;\n\n\t\tserver.listeningHost = this.host;\n\t\tserver.listeningPort = this.port;\n\n\t\tspawn(&serveImpl, cast(shared) this);\n\t\taddLogEntry(\"Started OneDrive API Webhook server\");\n\n\t\t// Subscriptions\n\t\toneDriveApiInstance = new OneDriveApi(this.appConfig);\n\t\toneDriveApiInstance.initialise();\n\t\tcreateOrRenewSubscription();\n\t}\n    \n    void stop() {\n        if (!this.started)\n            return;\n        server.stop();\n        this.started = false;\n\n\t\taddLogEntry(\"Stopped OneDrive API Webhook server\");\n\t\tobject.destroy(server);\n\n        // Delete subscription if there exists any\n\t\ttry {\n\t\t\tdeleteSubscription();\n\t\t} catch (OneDriveException e) {\n\t\t\tlogSubscriptionError(e);\n\t\t}\n\t\t// Release API instance back to the pool\n\t\toneDriveApiInstance.releaseCurlEngine();\n\t\tobject.destroy(oneDriveApiInstance);\n\t\toneDriveApiInstance = null;\n\t}\n\n\tprivate static void handle(shared OneDriveWebhook _this, Cgi cgi) {\n\t\tif (debugHTTPSResponse) {\n\t\t\taddLogEntry(\"Webhook request: \" ~ to!string(cgi.requestMethod) ~ \" \" ~ to!string(cgi.requestUri));\n\t\t\tif (!cgi.postBody.empty) {\n\t\t\t\taddLogEntry(\"Webhook post body: \" ~ to!string(cgi.postBody));\n\t\t\t}\n\t\t}\n\n\t\tcgi.setResponseContentType(\"text/plain\");\n\n\t\tif (\"validationToken\" in cgi.get)\t{\n\t\t\t// For validation requests, respond with the validation token passed in the query string\n\t\t\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/webhook-receiver-validation-request\n\t\t\tcgi.write(cgi.get[\"validationToken\"]);\n\t\t\taddLogEntry(\"OneDrive API Webhook: handled validation request\");\n\t\t} else {\n\t\t\t// Notifications don't include any information about the changes that triggered them.\n\t\t\t// Put a refresh signal in the queue and let the main monitor loop process it.\n\t\t\t// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/using-webhooks\n\t\t\t_this.count.atomicOp!\"+=\"(1);\n\t\t\tsend(cast()_this.parentTid, to!ulong(_this.count));\n\t\t\tcgi.write(\"OK\");\n\t\t\taddLogEntry(\"OneDrive API Webhook: sent refresh signal #\" ~ to!string(_this.count));\n\t\t}\n\t}\n\n    private static void serveImpl(shared OneDriveWebhook _this) {\n\t\t_this.server.serveEmbeddedHttp!(handle, OneDriveWebhook)(_this);\n\t}\n\n\t// Create a new subscription or renew the existing subscription\n\tvoid createOrRenewSubscription() {\n\t\tauto elapsed = Clock.currTime(UTC()) - subscriptionLastErrorAt;\n\t\tif (elapsed < subscriptionRetryInterval) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tif (!hasValidSubscription()) {\n\t\t\t\tcreateSubscription();\n\t\t\t} else if (isSubscriptionUpForRenewal()) {\n\t\t\t\trenewSubscription();\n\t\t\t}\n\t\t} catch (OneDriveException e) {\n\t\t\tlogSubscriptionError(e);\n\t\t\tsubscriptionLastErrorAt = Clock.currTime(UTC());\n\t\t\taddLogEntry(\"Will retry creating or renewing subscription in \" ~ to!string(subscriptionRetryInterval));\n\t\t} catch (JSONException e) {\n\t\t\taddLogEntry(\"ERROR: Unexpected JSON error when attempting to validate subscription: \" ~ e.msg);\n\t\t\tsubscriptionLastErrorAt = Clock.currTime(UTC());\n\t\t\taddLogEntry(\"Will retry creating or renewing subscription in \" ~ to!string(subscriptionRetryInterval));\n\t\t}\n\t}\n\n\t// Return the duration to next subscriptionExpiration check\n\tDuration getNextExpirationCheckDuration() {\n\t\tSysTime now = Clock.currTime(UTC());\n\t\tif (hasValidSubscription()) {\n\t\t\tDuration elapsed = Clock.currTime(UTC()) - subscriptionLastErrorAt;\n\t\t\t// Check if we are waiting for the next retry\n\t\t\tif (elapsed < subscriptionRetryInterval)\n\t\t\t\treturn subscriptionRetryInterval - elapsed;\n\t\t\telse \n\t\t\t\treturn subscriptionExpiration - now - subscriptionRenewalInterval;\n\t\t}\n\t\telse\n\t\t\treturn subscriptionRetryInterval;\n\t}\n\n\tprivate bool hasValidSubscription() {\n\t\treturn !subscriptionId.empty && subscriptionExpiration > Clock.currTime(UTC());\n\t}\n\n\tprivate bool isSubscriptionUpForRenewal() {\n\t\treturn subscriptionExpiration < Clock.currTime(UTC()) + subscriptionRenewalInterval;\n\t}\n\n\tprivate void createSubscription() {\n\t\taddLogEntry(\"Initialising webhook subscription for updates ...\");\n\n\t\tauto expirationDateTime = Clock.currTime(UTC()) + subscriptionExpirationInterval;\n\t\ttry {\n\t\t\tJSONValue response = oneDriveApiInstance.createSubscription(notificationUrl, expirationDateTime);\n\t\t\t// Save important subscription metadata including id and expiration\n\t\t\tsubscriptionId = response[\"id\"].str;\n\t\t\tsubscriptionExpiration = SysTime.fromISOExtString(response[\"expirationDateTime\"].str);\n\t\t\taddLogEntry(\"Created new subscription \" ~ subscriptionId ~ \" with expiration: \" ~ to!string(subscriptionExpiration.toISOExtString()));\n\t\t} catch (OneDriveException e) {\n\t\t\tif (e.httpStatusCode == 409) {\n\t\t\t\t// Take over an existing subscription on HTTP 409.\n\t\t\t\t//\n\t\t\t\t// Sample 409 error:\n\t\t\t\t// {\n\t\t\t\t// \t\"error\": {\n\t\t\t\t// \t\t\t\"code\": \"ObjectIdentifierInUse\",\n\t\t\t\t// \t\t\t\"innerError\": {\n\t\t\t\t// \t\t\t\t\t\"client-request-id\": \"615af209-467a-4ab7-8eff-27c1d1efbc2d\",\n\t\t\t\t// \t\t\t\t\t\"date\": \"2023-09-26T09:27:45\",\n\t\t\t\t// \t\t\t\t\t\"request-id\": \"615af209-467a-4ab7-8eff-27c1d1efbc2d\"\n\t\t\t\t// \t\t\t},\n\t\t\t\t// \t\t\t\"message\": \"Subscription Id c0bba80e-57a3-43a7-bac2-e6f525a76e7c already exists for the requested combination\"\n\t\t\t\t// \t}\n\t\t\t\t// }\n\n\t\t\t\t// Make sure the error code is \"ObjectIdentifierInUse\"\n\t\t\t\ttry {\n\t\t\t\t\tif (e.error[\"error\"][\"code\"].str != \"ObjectIdentifierInUse\") {\n\t\t\t\t\t\tthrow e;\n\t\t\t\t\t}\n\t\t\t\t} catch (JSONException jsonEx) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\n\t\t\t\t// Extract the existing subscription id from the error message\n\t\t\t\timport std.regex;\n\t\t\t\tauto idReg = ctRegex!(r\"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\", \"i\");\n\t\t\t\tauto m = matchFirst(e.error[\"error\"][\"message\"].str, idReg);\n\t\t\t\tif (!m) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\n\t\t\t\t// Save the subscription id and renew it immediately since we don't know the expiration timestamp\n\t\t\t\tsubscriptionId = m[0];\n\t\t\t\taddLogEntry(\"Found existing webhook subscription \" ~ subscriptionId);\n\t\t\t\trenewSubscription();\n\t\t\t} else {\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\t}\n\t\n\tprivate void renewSubscription() {\n\t\taddLogEntry(\"Renewing webhook subscription for updates ...\");\n\n\t\tauto expirationDateTime = Clock.currTime(UTC()) + subscriptionExpirationInterval;\n\t\ttry {\n\t\t\tJSONValue response = oneDriveApiInstance.renewSubscription(subscriptionId, expirationDateTime);\n\n\t\t\t// Update subscription expiration from the response\n\t\t\tsubscriptionExpiration = SysTime.fromISOExtString(response[\"expirationDateTime\"].str);\n\t\t\taddLogEntry(\"Renewed webhook subscription \" ~ subscriptionId ~ \" with expiration: \" ~ to!string(subscriptionExpiration.toISOExtString()));\n\t\t} catch (OneDriveException e) {\n\t\t\tif (e.httpStatusCode == 404) {\n\t\t\t\taddLogEntry(\"The subscription is not found on the server. Recreating subscription ...\");\n\t\t\t\tsubscriptionId = null;\n\t\t\t\tsubscriptionExpiration = Clock.currTime(UTC());\n\t\t\t\tcreateSubscription();\n\t\t\t} else {\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void deleteSubscription() {\n\t\tif (!hasValidSubscription()) {\n\t\t\treturn;\n\t\t}\n\t\toneDriveApiInstance.deleteSubscription(subscriptionId);\n\t\taddLogEntry(\"Deleted subscription\");\n\t}\n\t\n\tprivate void logSubscriptionError(OneDriveException e) {\n\t\tstring errorMsg;\n\n\t\ttry {\n\t\t\t// Attempt to extract the specific error message from the JSON if possible\n\t\t\tif (e.error.type == JSONType.object &&\n\t\t\t\t\"error\" in e.error &&\n\t\t\t\te.error[\"error\"].type == JSONType.object &&\n\t\t\t\t\"message\" in e.error[\"error\"]) {\n\t\t\t\terrorMsg = e.error[\"error\"][\"message\"].str;\n\t\t\t} else {\n\t\t\t\tthrow new Exception(\"Invalid error structure\");\n\t\t\t}\n\t\t} catch (Exception ex) {\n\t\t\t// Fallback to the message stored in the exception if the JSON is malformed or not structured as expected\n\t\t\terrorMsg = e.msg;\n\t\t}\n\n\t\t// Log a message to the GUI only\n\t\taddLogEntry(\"ERROR: An issue has occurred with webhook subscriptions: \" ~ errorMsg, [\"notify\"]);\n\n\t\t// Use the standard OneDrive API logging method\n\t\tdisplayOneDriveErrorMessage(errorMsg, getFunctionName!({}));\n\t}\n}"
  },
  {
    "path": "src/xattr.d",
    "content": "module xattr;\n\nimport core.sys.posix.sys.types;\nimport core.stdc.errno;\nimport core.stdc.stdlib;\nimport core.stdc.string;\nimport core.stdc.stdio;\nimport std.string;\nimport std.conv;\n\nversion (linux) {\n    extern (C) {\n        int setxattr(const(char)* path, const(char)* name, const(void)* value, size_t size, int flags);\n        ssize_t getxattr(const(char)* path, const(char)* name, void* value, size_t size);\n    }\n}\n\nversion (FreeBSD) {\n    extern (C) {\n        int extattr_set_file(const(char)* path, int attrnamespace, const(char)* name, const(void)* value, size_t size);\n        ssize_t extattr_get_file(const(char)* path, int attrnamespace, const(char)* name, void* value, size_t size);\n    }\n\n    enum EXTATTR_NAMESPACE_USER = 1;\n}\n\nclass XAttrException : Exception {\n    this(string message) {\n        super(message);\n    }\n}\n\n// Sets an extended attribute for a given file.\n// Throws `XAttrException` on failure.\nvoid setXAttr(string filePath, string attrName, string attrValue) {\n    version (linux) {\n        int result = setxattr(filePath.toStringz(), attrName.toStringz(), cast(const(void)*)attrValue.ptr, attrValue.length, 0);\n        if (result != 0) {\n            throw new XAttrException(\"Failed to set xattr '\" ~ attrName ~ \"' on '\" ~ filePath ~ \"': \" ~ to!string(strerror(errno)));\n        }\n    } else version (FreeBSD) {\n        int result = extattr_set_file(filePath.toStringz(), EXTATTR_NAMESPACE_USER, attrName.toStringz(), cast(const(void)*)attrValue.ptr, attrValue.length);\n        if (result < 0) {\n            throw new XAttrException(\"Failed to set xattr '\" ~ attrName ~ \"' on '\" ~ filePath ~ \"': \" ~ to!string(strerror(errno)));\n        }\n    } else {\n        throw new XAttrException(\"xattr not supported on this platform\");\n    }\n}\n\n// Retrieves an extended attribute value from a file.\n// Returns the attribute value as a string or throws `XAttrException` on failure.\nstring getXAttr(string filePath, string attrName) {\n    version (linux) {\n        // First, determine the size of the attribute value\n        ssize_t size = getxattr(filePath.toStringz(), attrName.toStringz(), null, 0);\n        if (size < 0) {\n            throw new XAttrException(\"Failed to get xattr size for '\" ~ attrName ~ \"' on '\" ~ filePath ~ \"': \" ~ to!string(strerror(errno)));\n        }\n\n        void* buffer = malloc(size);\n        scope(exit) free(buffer);\n\n        ssize_t ret = getxattr(filePath.toStringz(), attrName.toStringz(), buffer, cast(size_t)size);\n        if (ret < 0) {\n            throw new XAttrException(\"Failed to get xattr '\" ~ attrName ~ \"' on '\" ~ filePath ~ \"': \" ~ to!string(strerror(errno)));\n        }\n\n        return cast(string)(buffer[0 .. size]);\n    } else version (FreeBSD) {\n        // First, determine the size\n        ssize_t size = extattr_get_file(filePath.toStringz(), EXTATTR_NAMESPACE_USER, attrName.toStringz(), null, 0);\n        if (size < 0) {\n            throw new XAttrException(\"Failed to get xattr size for '\" ~ attrName ~ \"' on '\" ~ filePath ~ \"': \" ~ to!string(strerror(errno)));\n        }\n\n        void* buffer = malloc(size);\n        scope(exit) free(buffer);\n\n        ssize_t ret = extattr_get_file(filePath.toStringz(), EXTATTR_NAMESPACE_USER, attrName.toStringz(), buffer, cast(size_t)size);\n        if (ret < 0) {\n            throw new XAttrException(\"Failed to get xattr '\" ~ attrName ~ \"' on '\" ~ filePath ~ \"': \" ~ to!string(strerror(errno)));\n        }\n\n        return cast(string)(buffer[0 .. size]);\n    } else {\n        throw new XAttrException(\"xattr not supported on this platform\");\n    }\n}\n"
  },
  {
    "path": "tests/makefiles.sh",
    "content": "#!/bin/bash\nONEDRIVEALT=~/OneDriveALT\nif [ ! -d ${ONEDRIVEALT} ]; then\n        mkdir -p ${ONEDRIVEALT}\nelse\n        rm -rf ${ONEDRIVEALT}/*\nfi\n\nBADFILES=${ONEDRIVEALT}/bad_files\nTESTFILES=${ONEDRIVEALT}/test_files\nmkdir -p ${BADFILES}\nmkdir -p ${TESTFILES}\ndd if=/dev/urandom of=${TESTFILES}/large_file1.txt count=15 bs=1572864\ndd if=/dev/urandom of=${TESTFILES}/large_file2.txt count=20 bs=1572864\n\n# Create bad files that should be skipped\ntouch \"${BADFILES}/ leading_white_space\"\ntouch \"${BADFILES}/trailing_white_space \"\ntouch \"${BADFILES}/trailing_dot.\"\ntouch \"${BADFILES}/includes < in the filename\"\ntouch \"${BADFILES}/includes > in the filename\"\ntouch \"${BADFILES}/includes : in the filename\"\ntouch \"${BADFILES}/includes \\\" in the filename\"\ntouch \"${BADFILES}/includes | in the filename\"\ntouch \"${BADFILES}/includes ? in the filename\"\ntouch \"${BADFILES}/includes * in the filename\"\ntouch \"${BADFILES}/includes \\\\ in the filename\"\ntouch \"${BADFILES}/includes \\\\\\\\ in the filename\"\ntouch \"${BADFILES}/CON\"\ntouch \"${BADFILES}/CON.text\"\ntouch \"${BADFILES}/PRN\"\ntouch \"${BADFILES}/AUX\"\ntouch \"${BADFILES}/NUL\"\ntouch \"${BADFILES}/COM0\"\ntouch \"${BADFILES}/COM1\"\ntouch \"${BADFILES}/COM2\"\ntouch \"${BADFILES}/COM3\"\ntouch \"${BADFILES}/COM4\"\ntouch \"${BADFILES}/COM5\"\ntouch \"${BADFILES}/COM6\"\ntouch \"${BADFILES}/COM7\"\ntouch \"${BADFILES}/COM8\"\ntouch \"${BADFILES}/COM9\"\ntouch \"${BADFILES}/LPT0\"\ntouch \"${BADFILES}/LPT1\"\ntouch \"${BADFILES}/LPT2\"\ntouch \"${BADFILES}/LPT3\"\ntouch \"${BADFILES}/LPT4\"\ntouch \"${BADFILES}/LPT5\"\ntouch \"${BADFILES}/LPT6\"\ntouch \"${BADFILES}/LPT7\"\ntouch \"${BADFILES}/LPT8\"\ntouch \"${BADFILES}/LPT9\"\n\n# Test files from cases\n# File contains invalid whitespace characters\ntar xf ./bad-file-name.tar.xz -C ${BADFILES}/\n\n# HelloCOM2.rar should be allowed\ndd if=/dev/urandom of=${TESTFILES}/HelloCOM2.rar count=5 bs=1572864\n\n"
  }
]