Check-in [56d8711cc9]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Various improvements to ZTUpdater. Got rid of Html2Markdown, ported own poor man’s HtmlToText to C#, which appears to work better for this forum.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | develop
Files: files | file ages | folders
SHA1: 56d8711cc98c7851b7dc908d469e985db2ee77e5
User & Date: tinus 2018-03-12 02:28:55.367
Context
2018-03-21
19:53
Added options for archive and backup directories. check-in: 16163e0aca user: tinus tags: develop
2018-03-12
02:28
Various improvements to ZTUpdater. Got rid of Html2Markdown, ported own poor man’s HtmlToText to C#, which appears to work better for this forum. check-in: 56d8711cc9 user: tinus tags: develop
00:30
Added C# project for automatic updater for ZTreeWin. check-in: 355de8cbb4 user: tinus tags: develop
Changes
Unified Diff Ignore Whitespace Patch
Changes to ZTUpdater/Program.cs.
1

2
3
4
5
6
7
8
using System;

using System.Diagnostics;
using System.Globalization;

namespace ZTUpdater
{
    class Program
    {

>







1
2
3
4
5
6
7
8
9
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;

namespace ZTUpdater
{
    class Program
    {
16
17
18
19
20
21
22
23
24
25


26
27
28
29






30



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50

















                    if (arg == "/?" || arg.StartsWith("/h", true, CultureInfo.CurrentCulture))
                    {
                        ShowHelp();
                        return 0;
                    }
                }

                var Updater = new ZTWUpdater(true)
                {
                    OnLog = (msg, level) =>


                    {
                        var Output = level >= TraceLevel.Warning ? Console.Error : Console.Out;
                        Output.WriteLine(msg);
                    }






                };



                var UpdateTask = Updater.Update();
                UpdateTask.Wait();
                return UpdateTask.Result ? 0 : 1;
            }
            catch (Exception ex)
            {
                var BackupColor = Console.ForegroundColor;
                Console.ForegroundColor = ConsoleColor.Red;
                Console.Error.WriteLine(ex.Message);
                Console.ForegroundColor = BackupColor;
                return 255;
            }
        }

        private static void ShowHelp()
        {
            throw new NotImplementedException();
        }
    }

}
























|

|
>
>
|
<
<
<
>
>
>
>
>
>
|
>
>
>







|










|
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
17
18
19
20
21
22
23
24
25
26
27
28
29



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
                    if (arg == "/?" || arg.StartsWith("/h", true, CultureInfo.CurrentCulture))
                    {
                        ShowHelp();
                        return 0;
                    }
                }

                var Updater = new ZTWUpdater()
                {
                    OnLog = WriteLog
                };
                if (Updater.ZTreeHome == null)
                {



                    // ask for folder (using dialog?)
                    var Dialog = new System.Windows.Forms.FolderBrowserDialog
                    {
                        SelectedPath = @"C:\ZTree",
                        ShowNewFolderButton = false,
                        Description = "Please select the folder where ZTreeWin is installed"
                    };
                    if (Dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                       Updater.ZTreeHome = Dialog.SelectedPath;
                }
                var UpdateTask = Updater.Update();
                UpdateTask.Wait();
                return UpdateTask.Result ? 0 : 1;
            }
            catch (Exception ex)
            {
                var BackupColor = Console.ForegroundColor;
                Console.ForegroundColor = ConsoleColor.Magenta;
                Console.Error.WriteLine(ex.Message);
                Console.ForegroundColor = BackupColor;
                return 255;
            }
        }

        private static void ShowHelp()
        {
            throw new NotImplementedException();
        }

        private static Dictionary<TraceLevel, ConsoleColor> _levelColors = new Dictionary<TraceLevel, ConsoleColor>();

        private static void WriteLog(string message, TraceLevel level)
        {
            if (_levelColors.Count == 0)
            {
                _levelColors.Add(TraceLevel.Verbose, ConsoleColor.DarkGray);
                _levelColors.Add(TraceLevel.Warning, ConsoleColor.Yellow);
                _levelColors.Add(TraceLevel.Error, ConsoleColor.Red);
            }
            var Output = level >= TraceLevel.Warning ? Console.Error : Console.Out;
            var BackupColor = Console.ForegroundColor;
            if (_levelColors.TryGetValue(level, out var Color))
                Console.ForegroundColor = Color;
            Output.WriteLine(message);
            Console.ForegroundColor = BackupColor;
        }
    }
}
Changes to ZTUpdater/ZTUpdater.cs.
11
12
13
14
15
16
17
18
19
20
21
22
23





24
25


26




27

28
29
30
31
32

33
34
35
36


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

namespace ZTUpdater
{
    public class ZTWUpdater
    {
        public Action<string, TraceLevel> OnLog = (msg, level) => Debug.WriteLine(msg, level.ToString());

        public string ZTreeHome;
        public string Executable;
        public string CurrentVersion;
        public string LatestVersion;
        public string WhatsNew;






        public ZTWUpdater(bool askforLocationIfNecessary)
        {


            if (Environment.Is64BitOperatingSystem)




                Executable = "ZTW64.exe";

            else
                Executable = "ZTW.exe";
            
            // figure out where ZTreeWin is located
            ZTreeHome = GetZTreeHome(askforLocationIfNecessary);

            Executable = Path.Combine(ZTreeHome, Executable);

            // determine version of local executable
            CurrentVersion = FileVersionInfo.GetVersionInfo(Executable).FileVersion;


        }

        protected void Log(string message, TraceLevel level = TraceLevel.Verbose)
        {
            OnLog(message, level);
        }

        public string GetZTreeHome(bool askforLocationIfNecessary)
        {
            // from command-line arguments
            bool Found = false;
            foreach (var Arg in Environment.GetCommandLineArgs())
            {
                if (Found)
                    return Arg;
                else if (Arg == "/home")
                    Found = true;
            }

            // from the environment
            string ZTHome = Environment.GetEnvironmentVariable("#ZTHome");
            if (Directory.Exists(ZTHome))
                return ZTHome;

            // from registry (both Win32 and Win64)
            var InstallDir = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\ZTreeWin", "Install_Dir", null);
            if (InstallDir is string)
                return (string)InstallDir;
            if (Environment.Is64BitProcess)
            {
                InstallDir = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\ZTreeWin", "Install_Dir", null);
                if (InstallDir is string)
                    return (string)InstallDir;
            }

            // otherwise, ask for folder (using dialog?)
            if (askforLocationIfNecessary)
            {
                var Dialog = new System.Windows.Forms.FolderBrowserDialog
                {
                    SelectedPath = @"C:\ZTree",
                    ShowNewFolderButton = false,
                    Description = "Please select the folder where ZTreeWin is installed"
                };
                if (Dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                    return Dialog.SelectedPath;
            }
            return null;
        }

        private Regex _rexZetaAvailable = new Regex(@"forum_entry.php\?id=(\d+)""[^\<]*?v(\d+\.\S+) Now Available", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private Regex _rexWhatsNew = new Regex(@"<div class=""posting"">(.*?)</div><p class=""signature"">", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
        private Regex _rexLink = new Regex(@"""(https?://www.z(edtek|tree).com\/[^""]*\.(zip|exe))""", RegexOptions.Compiled | RegexOptions.IgnoreCase);








<
|
|
|
|

>
>
>
>
>
|

>
>
|
>
>
>
>
|
>
|
<
|
|
|
>
|

|
|
>
>







|


















|
|
<
<
|
|
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<







11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77


78
79
80
81













82
83
84
85
86
87
88

namespace ZTUpdater
{
    public class ZTWUpdater
    {
        public Action<string, TraceLevel> OnLog = (msg, level) => Debug.WriteLine(msg, level.ToString());


        public string Executable { get; set; } = Environment.Is64BitOperatingSystem ? "ZTW64.exe" : "ZTW.exe";
        public string CurrentVersion { get; private set; }
        public string LatestVersion { get; private set; }
        public string WhatsNew { get; private set; }

        private string _ZTreeHome;

        public ZTWUpdater() : this(GetZTreeHome())
        {
        }
        public ZTWUpdater(string ztreeHome)
        {
            ZTreeHome = ztreeHome;
        }

        public string ZTreeHome
        {
            get
            {
                return _ZTreeHome;
            }
            set

            {
                _ZTreeHome = value;
                if (_ZTreeHome != null)
                {
                    Executable = Path.Combine(ZTreeHome, Executable);

                    // determine version of local executable
                    CurrentVersion = FileVersionInfo.GetVersionInfo(Executable).FileVersion;
                }
            }
        }

        protected void Log(string message, TraceLevel level = TraceLevel.Verbose)
        {
            OnLog(message, level);
        }

        public static string GetZTreeHome()
        {
            // from command-line arguments
            bool Found = false;
            foreach (var Arg in Environment.GetCommandLineArgs())
            {
                if (Found)
                    return Arg;
                else if (Arg == "/home")
                    Found = true;
            }

            // from the environment
            string ZTHome = Environment.GetEnvironmentVariable("#ZTHome");
            if (Directory.Exists(ZTHome))
                return ZTHome;

            // from registry (both Win32 and Win64)
            var InstallDir = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\ZTreeWin", "Install_Dir", null);
            if (InstallDir is string && Directory.Exists(ZTHome = (string)InstallDir))
                return ZTHome;


            InstallDir = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\ZTreeWin", "Install_Dir", null);
            if (InstallDir is string && Directory.Exists(ZTHome = (string)InstallDir))
                return ZTHome;














            return null;
        }

        private Regex _rexZetaAvailable = new Regex(@"forum_entry.php\?id=(\d+)""[^\<]*?v(\d+\.\S+) Now Available", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private Regex _rexWhatsNew = new Regex(@"<div class=""posting"">(.*?)</div><p class=""signature"">", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
        private Regex _rexLink = new Regex(@"""(https?://www.z(edtek|tree).com\/[^""]*\.(zip|exe))""", RegexOptions.Compiled | RegexOptions.IgnoreCase);

117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
                // extract the "What’s New"
                Match = _rexWhatsNew.Match(Html);
                if (Match.Success)
                {
                    WhatsNew = Match.Groups[1].Value;
                    try
                    {
                        WhatsNew = new Html2Markdown.Converter().Convert(WhatsNew);
                    }
                    catch
                    {
                        // ignore errors, just use the HTML instead
                    }
                    Log("What’s New:" + Environment.NewLine + WhatsNew, TraceLevel.Info);
                }

                // extract the download link
                Match = _rexLink.Match(Html);
                if (!Match.Success)
                {
                    Log($"No download link found for version {LatestVersion}!", TraceLevel.Warning);
                    return null;
                }
                // return the link
                Log("Download link found: " + Match.Groups[1].Value);
                return new Uri(Match.Groups[1].Value);
            }
        }

        public bool IsVersionNewer(string newVersion, string oldVersion)
        {
            var New = new Version(newVersion);
            var Old = new Version(oldVersion);
            return newVersion.CompareTo(oldVersion) >= 0;
        }

        public async Task<bool> Update()
        {
            Log($"ZTreeWin version {CurrentVersion} located in \"{ZTreeHome}\".", TraceLevel.Info);

            var DownloadUri = await GetUriLatestVersion();







|





|



















|







115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
                // extract the "What’s New"
                Match = _rexWhatsNew.Match(Html);
                if (Match.Success)
                {
                    WhatsNew = Match.Groups[1].Value;
                    try
                    {
                        WhatsNew = Utils.HtmlToText(WhatsNew);
                    }
                    catch
                    {
                        // ignore errors, just use the HTML instead
                    }
                    Log($"What’s New in v{LatestVersion}:" + Environment.NewLine + WhatsNew, TraceLevel.Info);
                }

                // extract the download link
                Match = _rexLink.Match(Html);
                if (!Match.Success)
                {
                    Log($"No download link found for version {LatestVersion}!", TraceLevel.Warning);
                    return null;
                }
                // return the link
                Log("Download link found: " + Match.Groups[1].Value);
                return new Uri(Match.Groups[1].Value);
            }
        }

        public bool IsVersionNewer(string newVersion, string oldVersion)
        {
            var New = new Version(newVersion);
            var Old = new Version(oldVersion);
            return newVersion.CompareTo(oldVersion) > 0;
        }

        public async Task<bool> Update()
        {
            Log($"ZTreeWin version {CurrentVersion} located in \"{ZTreeHome}\".", TraceLevel.Info);

            var DownloadUri = await GetUriLatestVersion();
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
            int Counter = 0;
            while (File.Exists(ZipFile))
            {
                Counter++;
                ZipFile = Base + $" ({Counter})" + Extension;
            }

            Log($@"Downloading updated file to ""{ZipFile}""...");
            using (var Client = new HttpClient())
            {
                var ZipStream = await Client.GetStreamAsync(DownloadUri);
                using (var FileStream = new FileStream(ZipFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read))
                {
                    await ZipStream.CopyToAsync(FileStream);
                }







|







169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
            int Counter = 0;
            while (File.Exists(ZipFile))
            {
                Counter++;
                ZipFile = Base + $" ({Counter})" + Extension;
            }

            Log($@"Downloading update file to ""{OrgZipFile}""...");
            using (var Client = new HttpClient())
            {
                var ZipStream = await Client.GetStreamAsync(DownloadUri);
                using (var FileStream = new FileStream(ZipFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read))
                {
                    await ZipStream.CopyToAsync(FileStream);
                }
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
            foreach(var Entry in Zip.Entries)
            {
                var File = new FileInfo(Path.Combine(ZTreeHome, Entry.FullName.Replace('/', '\\')));
                if (File.Exists)
                {
                    if (Identical(Entry, File))
                    {
                        Log($"\t{Entry.FullName}: identical to existing file; skipping.");
                        continue;
                    }

                    // rename as backup for current version
                    var Extension = File.Extension;
                    var BackupName = File.FullName.Substring(0, File.FullName.Length - Extension.Length) + "-v" + CurrentVersion + Extension;
                    Log($"\t{Entry.FullName}: backing up existing file.");
                    File.MoveTo(BackupName);
                }

                Log($"\t{Entry.FullName}: extracting...");
                Entry.ExtractToFile(File.FullName);
            }
        }

        private bool Identical(ZipArchiveEntry Entry, FileInfo File)
        {
            if (Entry.Length != File.Length)







|






|



|







209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
            foreach(var Entry in Zip.Entries)
            {
                var File = new FileInfo(Path.Combine(ZTreeHome, Entry.FullName.Replace('/', '\\')));
                if (File.Exists)
                {
                    if (Identical(Entry, File))
                    {
                        Log($"\t{Entry.FullName}\tidentical to existing file; skipping.");
                        continue;
                    }

                    // rename as backup for current version
                    var Extension = File.Extension;
                    var BackupName = File.FullName.Substring(0, File.FullName.Length - Extension.Length) + "-v" + CurrentVersion + Extension;
                    Log($"\t{Entry.FullName}\tbacking up existing file.");
                    File.MoveTo(BackupName);
                }

                Log($"\t{Entry.FullName}\textracting...");
                Entry.ExtractToFile(File.FullName);
            }
        }

        private bool Identical(ZipArchiveEntry Entry, FileInfo File)
        {
            if (Entry.Length != File.Length)
257
258
259
260
261
262
263
264







































































                AtEndOfB = b.Read(BufferB, 0, BufferSize) == 0;
                if (!BufferA.SequenceEqual(BufferB))
                    return false;
            }
            return (AtEndOfA == AtEndOfB);
        }
    }
}














































































|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
                AtEndOfB = b.Read(BufferB, 0, BufferSize) == 0;
                if (!BufferA.SequenceEqual(BufferB))
                    return false;
            }
            return (AtEndOfA == AtEndOfB);
        }
    }

    static class Utils
    {
        public static string HtmlToText(string Html)
        {
            // Normalize whitespace
            var Text = Regex.Replace(Html, @"\s+", " ");

            // Convert some simple formatting tags
            Text = Regex.Replace(Text, @"</?(b|strong)\b[^>]*>", "**", RegexOptions.IgnoreCase);
            Text = Regex.Replace(Text, @"</?(i|em)\b[^>]*>", "/", RegexOptions.IgnoreCase);
            Text = Regex.Replace(Text, @"</?(u)\b[^>]*>", "_", RegexOptions.IgnoreCase);
            Text = Regex.Replace(Text, @"</?(br|p)\b[^>]*>", "\r\n", RegexOptions.IgnoreCase);
            Text = Regex.Replace(Text, @"</?(hr)\b[^>]*>", "\r\n--------------------\r\n", RegexOptions.IgnoreCase);
            Text = Regex.Replace(Text, @"(\r\n|\r|\n)<li\b[^>]*>", "\r\n- ", RegexOptions.IgnoreCase);

            // get rid of all other HTML tags
            Text = Regex.Replace(Text, @"<[^>]*>", "");

            // decode all HTML entities
            var rexEntity = new Regex(@"&(#x?[0-9a-f]{1,4}|\w{2,8});", RegexOptions.Compiled | RegexOptions.IgnoreCase);
            Match Match;
            int StartAt = 0;
            while ((Match = rexEntity.Match(Text, StartAt)).Success)
            {
                var Entity = Match.Groups[1].Value;
                var Replacement = "?";
                if (Entity.StartsWith("#"))
                {
                    int CharCode = 0;
                    if (Entity.Substring(1, 1) == "x")
                        // hexadecimal char code
                        CharCode = int.Parse(Entity.Substring(2), System.Globalization.NumberStyles.HexNumber);
                    else
                        // decimal char code
                        CharCode = int.Parse(Entity.Substring(1));
                    Replacement = Char.ConvertFromUtf32(CharCode);
                }
                else
                {
                    switch (Entity)
                    {
                        case "amp":
                            Replacement = "&";
                            break;
                        case "lt":
                            Replacement = "<";
                            break;
                        case "gt":
                            Replacement = ">";
                            break;
                        case "quot":
                            Replacement = "\"";
                            break;
                        case "apos":
                            Replacement = "'";
                            break;
                        default:
                            // if we don't recognize this entity, just pick its first character
                            Replacement = Entity.Substring(0, 1);
                            break;
                    }

                }
                Text = Text.Substring(0, Match.Index) + Replacement + Text.Substring(Match.Index + Match.Length);
                StartAt = Match.Index + Replacement.Length;
            }

            return Text;
        }
    }
}
Changes to ZTUpdater/ZTUpdater.csproj.
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="Html2Markdown, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>packages\Html2Markdown.3.2.1.341\lib\net45\Html2Markdown.dll</HintPath>
    </Reference>
    <Reference Include="HtmlAgilityPack, Version=1.7.1.0, Culture=neutral, PublicKeyToken=bd319b19eaf3b43a, processorArchitecture=MSIL">
      <HintPath>packages\HtmlAgilityPack.1.7.1\lib\Net45\HtmlAgilityPack.dll</HintPath>
    </Reference>
    <Reference Include="Syroot.Windows.IO.KnownFolders, Version=1.0.2.0, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>packages\Syroot.Windows.IO.KnownFolders.1.0.2\lib\net40\Syroot.Windows.IO.KnownFolders.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.IO.Compression" />
    <Reference Include="System.IO.Compression.FileSystem" />







<
<
<
<
<
<







28
29
30
31
32
33
34






35
36
37
38
39
40
41
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>






    <Reference Include="Syroot.Windows.IO.KnownFolders, Version=1.0.2.0, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>packages\Syroot.Windows.IO.KnownFolders.1.0.2\lib\net40\Syroot.Windows.IO.KnownFolders.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.IO.Compression" />
    <Reference Include="System.IO.Compression.FileSystem" />
Changes to ZTUpdater/packages.config.
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Html2Markdown" version="3.2.1.341" targetFramework="net461" />
  <package id="HtmlAgilityPack" version="1.7.1" targetFramework="net461" />
  <package id="Syroot.Windows.IO.KnownFolders" version="1.0.2" targetFramework="net461" />
</packages>


<
<


1
2


3
4
<?xml version="1.0" encoding="utf-8"?>
<packages>


  <package id="Syroot.Windows.IO.KnownFolders" version="1.0.2" targetFramework="net461" />
</packages>