├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SunSync.sln ├── SunSync ├── .vs │ └── SunSync │ │ └── v14 │ │ └── .suo ├── AccountSettingPage.xaml ├── AccountSettingPage.xaml.cs ├── App.xaml ├── App.xaml.cs ├── DomainsSettingPage.xaml ├── DomainsSettingPage.xaml.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Models │ ├── Account.cs │ ├── BucketFileInfo.cs │ ├── CachedHash.cs │ ├── Domains.cs │ ├── FileUploader.cs │ ├── Log.cs │ ├── LogExporter.cs │ ├── PutRet.cs │ ├── SyncLog.cs │ ├── SyncRecord.cs │ ├── SyncSetting.cs │ ├── SystemConfig.cs │ ├── Tools.cs │ └── UploadInfo.cs ├── Pictures │ ├── back.png │ ├── cloud.png │ ├── folder.png │ ├── home.png │ ├── qiniu_logo.jpg │ ├── reload.png │ ├── result.png │ ├── sun_logo.jpg │ └── sunsync.ico ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ ├── Settings.settings │ └── app.manifest ├── QuickStartPage.xaml ├── QuickStartPage.xaml.cs ├── SunSync.csproj ├── SunSync.csproj.user ├── SunSync.sln ├── SunSync.v11.suo ├── SyncProgressPage.xaml ├── SyncProgressPage.xaml.cs ├── SyncResultPage.xaml ├── SyncResultPage.xaml.cs ├── SyncSettingPage.xaml ├── SyncSettingPage.xaml.cs ├── app.config ├── images │ ├── back.png │ ├── cloud.png │ ├── folder.png │ ├── home.png │ ├── qiniu_logo.jpg │ ├── reload.png │ ├── result.png │ ├── sun_logo.jpg │ └── sunsync.ico ├── packages.config ├── sunsync.ico └── sunsync.pfx ├── docs ├── QSunSync-v1.7.0-使用手册.html ├── imgs │ ├── 1-01.png │ ├── 1-02.png │ ├── 2-01.png │ ├── 2-02.png │ ├── 2-03.png │ ├── 3-01.png │ ├── 3-02.png │ ├── 3-03.png │ ├── 3-04-x.png │ ├── 3-04.png │ ├── 3-05-1.png │ ├── 3-05-2.png │ ├── 3-06.png │ ├── 3-07.png │ ├── 4-01.png │ ├── 4-02.png │ └── 5-01.png ├── qsunbox-太阳队.pptx ├── qsunbox.jpg ├── 七牛云文件同步工具v1.pdf └── 七牛云文件同步工具文档.pdf └── lib └── System.Data.SQLite.dll /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | obj 10 | bin 11 | obj/Debug 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | packages/ 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.1.1 (2018-03-07) 2 | 3 | * 修复低频存储无效的问题 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jemy Graw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QSunSync 七牛云文件同步工具 2 | 3 | ## 简单介绍 4 | 5 | `QSunSync`是使用`C# + WPF`开发的用于将本地文件同步到七牛云端空间的Windows客户端。该客户端的特点就是简单易用,而且方便增量同步。 6 | 7 | ## 下载安装 8 | 9 | ![下载图标](http://devtools.qiniu.com/download.png?imageView2/0/w/16) [QSunSync-v2.1.1](http://devtools.qiniu.com/QSunSync-v2.1.1.zip) 10 | 11 | 1. 该软件的使用需要`.NET Framework 4.0`支持,可以从 [微软官方下载中心](https://www.microsoft.com/zh-cn/download/details.aspx?id=17718) 下载安装。 12 | 2. 该软件使用了`SQLite`数据库来记录本地文件的hash值,所以需要在`.NET Framework4.0`安装完成之后,安装`SQLite`支持软件,这个可以从 [这里]( 13 | http://devtools.qiniu.com/sqlite_net4.0.exe) 下载,该软件仅仅是一个依赖库,不会对原有系统稳定性造成影响。 14 | 3. 然后下载`QSunSync`解压缩后,双击 `QSunSync.exe` 打开就可以使用了。 15 | 16 | 备注:如果使用的是旧版本,请先删除 "我的文档" 目录下的文件夹 `qsunsync` 。 17 | 18 | ## 功能介绍 19 | 20 | 该软件支持如下功能: 21 | 22 | 1. 可以将指定文件夹中文件完整同步到目标空间,默认以文件在文件夹中的相对路径作为文件名 23 | 2. 可以在上传之前,对空间中同名文件进行检查,如果发现同名文件则根据强制覆盖条件的设置来决定是否覆盖 24 | 3. 可以给上传到空间的文件指定一个额外的前缀 25 | 4. 可以忽略文件名称相对于同步目录的相对路径,直接以文件本身的名字来命名 26 | 5. 可以根据上传的机器位置选择合适的入口域名 27 | 6. 可以根据文件的平均大小和实际带宽设置一个合理的并发数量 28 | 7. 可以根据实际带宽的情况,选择分片上传的片的大小,带宽越大,片大小可以选择越大,效率越高 29 | 8. 支持单文件断点续传,支持目录增量同步 30 | 31 | ## 自行编译 32 | 33 | 如果你打算自行编译这个项目的话,请按照如下方式: 34 | 35 | 1. 这个项目是使用 Visual Studio 2015 开发的,所以这个版本以上的都可以; 36 | 2. 这个项目依赖另外一个项目提供的SDK,这个项目是 [csharp-sdk](https://github.com/qiniu/csharp-sdk); 37 | 3. 然后,编译吧。 38 | 39 | 40 | ## 使用方式 41 | 42 | 1. 首次打开软件的时候,需要进行帐号设置才能去“新建同步任务”,七牛云存储的文件上传使用一对密钥`AK/SK`来进行权限校验,这一对密钥在七牛云存储的后台里面是可以找到的。 43 | 2. 你可以直接到“帐号设置”里面点击“查看我的AS&SK”,这将自动帮你打开浏览器并导向到`AK/SK`的所在地,你直接拷贝,粘贴到本地的输入框里面就好了,输入完成之后,点击“保存” 44 | 就可以了,当然如果你输入了错误的`AK&SK`,你会收到错误提示的,嘿嘿。 45 | 3. 帐号设置完成之后,就可以“新建同步任务”了,在“同步设置”的“基本设置”里面,你可以选择本地待同步目录和希望同步到的云端空间即可,如果需要更多的设置,可以看“高级设置”。 46 | 4. 设置完成之后,你就可以点击“开始同步”进行同步了。 47 | 48 | 49 | ## 意见&帮助 50 | 51 | 如果你有任何的意见,可以通过提 issue,我们来讨论。如果你需要帮助,可以加入群 (QQ: 343822521),非技术勿扰。 52 | -------------------------------------------------------------------------------- /SunSync.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2002 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SunSync", "SunSync\SunSync.csproj", "{1D91C666-3321-4ED9-9777-889CC4BAB795}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|Any CPU = Release|Any CPU 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|x64.ActiveCfg = Debug|x64 21 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|x64.Build.0 = Debug|x64 22 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|x86.ActiveCfg = Debug|x86 23 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|x86.Build.0 = Debug|x86 24 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|x64.ActiveCfg = Release|Any CPU 27 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|x64.Build.0 = Release|Any CPU 28 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|x64.Deploy.0 = Release|Any CPU 29 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|x86.ActiveCfg = Release|x86 30 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|x86.Build.0 = Release|x86 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(ExtensibilityGlobals) = postSolution 36 | SolutionGuid = {541FD683-9BBC-4094-B0A2-209BB8F0F6B0} 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /SunSync/.vs/SunSync/v14/.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/.vs/SunSync/v14/.suo -------------------------------------------------------------------------------- /SunSync/AccountSettingPage.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | AK & SK 是七牛用来进行API权限控制的一对密钥。 33 | 安全规范使用 AK & SK 是保障数据安全的重要环节。 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 友情提示: 55 | 56 | 57 | 1. 一个账号最多拥有两对密钥(Access/Secret Key)。 58 | 59 | 60 | 2. 更换密钥时,请创建第二个密钥。 61 | 62 | 63 | 3. 删除密钥前须停用。 64 | 65 | 66 | 4. 出于安全考虑,建议您周期性地更换密钥。 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /SunSync/AccountSettingPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using Newtonsoft.Json; 6 | using SunSync.Models; 7 | using System.IO; 8 | using System.Threading; 9 | using Qiniu.Util; 10 | using Qiniu.Storage; 11 | namespace SunSync 12 | { 13 | /// 14 | /// Interaction logic for AccountSettingPage.xaml 15 | /// 16 | public partial class AccountSettingPage : Page 17 | { 18 | private MainWindow mainWindow; 19 | private string myAKSKLink = "https://portal.qiniu.com/user/key"; 20 | public AccountSettingPage(MainWindow mainWindow) 21 | { 22 | InitializeComponent(); 23 | this.mainWindow = mainWindow; 24 | this.loadAccountInfo(); 25 | this.SettingsErrorTextBlock.Text = ""; 26 | } 27 | 28 | private void BackToHome_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) 29 | { 30 | this.mainWindow.GotoHomePage(); 31 | } 32 | 33 | /// 34 | /// view my ak & sk button click handler 35 | /// 36 | /// 37 | /// 38 | private void ViewMyAKSK_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) 39 | { 40 | try 41 | { 42 | System.Diagnostics.Process.Start(this.myAKSKLink); 43 | } 44 | catch (Exception ex) 45 | { 46 | Log.Error("open ak & sk link failed, " + ex.Message); 47 | } 48 | } 49 | 50 | /// 51 | /// load ak & sk from local file 52 | /// 53 | private void loadAccountInfo() 54 | { 55 | Account acct = Account.TryLoadAccount(); 56 | if (!string.IsNullOrEmpty(acct.AccessKey)) 57 | { 58 | this.AccessKeyTextBox.Text = acct.AccessKey; 59 | } 60 | if (!string.IsNullOrEmpty(acct.SecretKey)) 61 | { 62 | this.SecretKeyTextBox.Text = acct.SecretKey; 63 | } 64 | } 65 | 66 | /// 67 | /// save account settings to local file and check the validity of the settings 68 | /// 69 | private void SaveAccountSetting(object accountObj) 70 | { 71 | Account account = (Account)accountObj; 72 | //write settings to local file 73 | string accData = JsonConvert.SerializeObject(account); 74 | string myDocPath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); 75 | string appDir = System.IO.Path.Combine(myDocPath, "qsunsync"); 76 | try 77 | { 78 | if (!Directory.Exists(appDir)) 79 | { 80 | try 81 | { 82 | Directory.CreateDirectory(appDir); 83 | } 84 | catch (Exception ex) 85 | { 86 | Log.Error(string.Format("create app dir {0} failed due to {1}", appDir, ex.Message)); 87 | Dispatcher.Invoke(new Action(delegate 88 | { 89 | this.SettingsErrorTextBlock.Text = "创建本地配置路径失败"; 90 | })); 91 | } 92 | } 93 | string accPath = System.IO.Path.Combine(appDir, "account.json"); 94 | using (StreamWriter sw = new StreamWriter(accPath, false, Encoding.UTF8)) 95 | { 96 | sw.Write(accData); 97 | } 98 | } 99 | catch (Exception ex) 100 | { 101 | Log.Error("save account info to file failed, " + ex.Message); 102 | Dispatcher.Invoke(new Action(delegate 103 | { 104 | this.SettingsErrorTextBlock.Text = "帐号设置写入文件失败"; 105 | })); 106 | } 107 | 108 | //check ak & sk validity 109 | Mac mac = new Mac(account.AccessKey, account.SecretKey); 110 | //use fixed zone to avoid the uc query 111 | Config config = new Config(); 112 | //init domains 113 | Domains domains = Domains.TryLoadDomains(); 114 | if (domains != null && !string.IsNullOrEmpty(domains.RsDomain)) 115 | { 116 | Qiniu.Storage.Config.DefaultRsHost = domains.RsDomain; 117 | config.Zone = new Zone 118 | { 119 | RsHost = domains.RsDomain, 120 | }; 121 | } 122 | else 123 | { 124 | config.Zone = Zone.ZONE_CN_East; 125 | } 126 | BucketManager bucketManager = new BucketManager(mac, config); 127 | StatResult statResult = bucketManager.Stat("NONE_EXIST_BUCKET", "NONE_EXIST_KEY"); 128 | 129 | 130 | if (statResult.Code == 401) 131 | { 132 | Log.Error("ak & sk wrong"); 133 | Dispatcher.Invoke(new Action(delegate 134 | { 135 | this.SettingsErrorTextBlock.Text = "AK 或 SK 设置不正确"; 136 | })); 137 | } 138 | else if (statResult.Code == 612 || statResult.Code == 631) 139 | { 140 | Log.Info("ak & sk is valid"); 141 | Dispatcher.Invoke(new Action(delegate 142 | { 143 | this.SettingsErrorTextBlock.Text = "AK & SK 设置正确!"; 144 | })); 145 | Dispatcher.Invoke(new Action(delegate 146 | { 147 | this.mainWindow.GotoHomePage(); 148 | })); 149 | } 150 | else 151 | { 152 | Log.Error("stat file network error, " + statResult.Text); 153 | string message = null; 154 | if (!string.IsNullOrEmpty(statResult.RefText)) 155 | { 156 | message = string.Format("验证帐号失败 {0}", statResult.RefText); 157 | } 158 | else 159 | { 160 | message = "验证帐号失败,网络故障!"; 161 | } 162 | 163 | Dispatcher.Invoke(new Action(delegate 164 | { 165 | this.SettingsErrorTextBlock.Text = message; 166 | })); 167 | } 168 | } 169 | 170 | 171 | /// 172 | /// save account settings button click handler 173 | /// 174 | /// 175 | /// 176 | private void SaveAccountSettings_EventHandler(object sender, System.Windows.RoutedEventArgs e) 177 | { 178 | string accessKey = this.AccessKeyTextBox.Text.Trim(); 179 | string secretKey = this.SecretKeyTextBox.Text.Trim(); 180 | Account account = new Account(); 181 | account.AccessKey = accessKey; 182 | account.SecretKey = secretKey; 183 | new Thread(new ParameterizedThreadStart(this.SaveAccountSetting)).Start(account); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /SunSync/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /SunSync/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | 4 | namespace SunSync 5 | { 6 | /// 7 | /// Interaction logic for App.xaml 8 | /// 9 | public partial class App : Application 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SunSync/DomainsSettingPage.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 域名设置用来自定义上传(UP)服务和资源管理(RS)服务所使用的域名,适用于私有云部署场景。 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 友情提示: 56 | 57 | 58 | 1. 普通公有云服务不需要填写这里的域名; 59 | 60 | 61 | 2. 私有云部署情况下,请填写正确的域名; 62 | 63 | 64 | 3. 填写的域名不需要带 http:// 的协议; 65 | 66 | 67 | 4. 填写的具体域名请咨询七牛服务工程师。 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /SunSync/DomainsSettingPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Qiniu.Storage; 3 | using Qiniu.Util; 4 | using SunSync.Models; 5 | using System; 6 | using System.IO; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Windows.Controls; 10 | using System.Windows.Input; 11 | namespace SunSync 12 | { 13 | /// 14 | /// Interaction logic for AccountSettingPage.xaml 15 | /// 16 | public partial class DomainsSettingPage : Page 17 | { 18 | private MainWindow mainWindow; 19 | public DomainsSettingPage(MainWindow mainWindow) 20 | { 21 | InitializeComponent(); 22 | this.mainWindow = mainWindow; 23 | this.loadDomainsInfo(); 24 | this.SettingsErrorTextBlock.Text = ""; 25 | } 26 | 27 | private void BackToHome_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) 28 | { 29 | this.mainWindow.GotoHomePage(); 30 | } 31 | 32 | 33 | /// 34 | /// load ak & sk from local file 35 | /// 36 | private void loadDomainsInfo() 37 | { 38 | Domains domains = Domains.TryLoadDomains(); 39 | if (!string.IsNullOrEmpty(domains.UpDomain)) 40 | { 41 | this.UpDomainTextBox.Text = domains.UpDomain; 42 | } 43 | if (!string.IsNullOrEmpty(domains.RsDomain)) 44 | { 45 | this.RsDomainTextBox.Text = domains.RsDomain; 46 | } 47 | } 48 | 49 | /// 50 | /// save account settings to local file and check the validity of the settings 51 | /// 52 | private void SaveDomainsSetting(object domainsObj) 53 | { 54 | Domains account = (Domains)domainsObj; 55 | //write settings to local file 56 | string domainsData = JsonConvert.SerializeObject(account); 57 | string myDocPath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); 58 | string appDir = System.IO.Path.Combine(myDocPath, "qsunsync"); 59 | try 60 | { 61 | if (!Directory.Exists(appDir)) 62 | { 63 | try 64 | { 65 | Directory.CreateDirectory(appDir); 66 | } 67 | catch (Exception ex) 68 | { 69 | Log.Error(string.Format("create app dir {0} failed due to {1}", appDir, ex.Message)); 70 | Dispatcher.Invoke(new Action(delegate 71 | { 72 | this.SettingsErrorTextBlock.Text = "创建本地配置路径失败"; 73 | })); 74 | } 75 | } 76 | string domainsPath = System.IO.Path.Combine(appDir, "domains.json"); 77 | using (StreamWriter sw = new StreamWriter(domainsPath, false, Encoding.UTF8)) 78 | { 79 | sw.Write(domainsData); 80 | } 81 | Dispatcher.Invoke(new Action(delegate 82 | { 83 | this.SettingsErrorTextBlock.Text = "设置成功!"; 84 | Thread.Sleep(2000); 85 | this.mainWindow.GotoHomePage(); 86 | })); 87 | } 88 | catch (Exception ex) 89 | { 90 | Log.Error("save domains info to file failed, " + ex.Message); 91 | Dispatcher.Invoke(new Action(delegate 92 | { 93 | this.SettingsErrorTextBlock.Text = "域名设置写入文件失败"; 94 | })); 95 | } 96 | 97 | 98 | } 99 | 100 | private void SaveDomainsSettings_EventHandler(object sender, System.Windows.RoutedEventArgs e) 101 | { 102 | string upDomain = this.UpDomainTextBox.Text.Trim(); 103 | string rsDomain = this.RsDomainTextBox.Text.Trim(); 104 | Domains domains = new Domains(); 105 | domains.UpDomain = upDomain; 106 | domains.RsDomain = rsDomain; 107 | new Thread(new ParameterizedThreadStart(this.SaveDomainsSetting)).Start(domains); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /SunSync/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SunSync/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using SunSync.Models; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Forms; 5 | 6 | namespace SunSync 7 | { 8 | /// 9 | /// Interaction logic for MainWindow.xaml 10 | /// 11 | public partial class MainWindow : Window 12 | { 13 | private NotifyIcon nIcon; 14 | private QuickStartPage quickStartPage; 15 | private AccountSettingPage accountSettingPage; 16 | private DomainsSettingPage domainsSettingPage; 17 | private SyncSettingPage syncSettingPage; 18 | private SyncProgressPage syncProgressPage; 19 | private SyncResultPage syncResultPage; 20 | public MainWindow() 21 | { 22 | InitializeComponent(); 23 | //init log 24 | Log.Init(); 25 | 26 | try 27 | { 28 | string version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); 29 | this.Title =string.Format("{0} v{1}", this.Title,version); 30 | //init tray 31 | this.nIcon = new NotifyIcon(); 32 | this.nIcon.Text = "QSunSync 七牛云文件同步"; 33 | this.nIcon.BalloonTipText = "QSunSync 七牛云文件同步"; 34 | this.nIcon.Icon = new System.Drawing.Icon("sunsync.ico"); 35 | this.nIcon.Visible = false; 36 | this.nIcon.MouseDoubleClick += NotifyIcon_MouseDoubleClick_EventHandler; 37 | this.nIcon.ShowBalloonTip(2000); 38 | MenuItem exitItem = new MenuItem("退出 QSunSync"); 39 | exitItem.Click += ExitApp_EventHandler; 40 | ContextMenu ctxMenu = new ContextMenu(new MenuItem[] { exitItem }); 41 | this.nIcon.ContextMenu = ctxMenu; 42 | } 43 | catch (Exception ex) 44 | { 45 | Log.Error("init the tray icon failed, " + ex.Message); 46 | } 47 | 48 | //init pages 49 | this.quickStartPage = new QuickStartPage(this); 50 | this.accountSettingPage = new AccountSettingPage(this); 51 | this.domainsSettingPage = new DomainsSettingPage(this); 52 | this.syncSettingPage = new SyncSettingPage(this); 53 | this.syncProgressPage = new SyncProgressPage(this); 54 | this.syncResultPage = new SyncResultPage(this); 55 | } 56 | 57 | private void ExitApp_EventHandler(object sender, EventArgs e) 58 | { 59 | MessageBoxResult msgResult = System.Windows.MessageBox.Show("确认退出 QSunSync?", "退出 QSunSync", 60 | MessageBoxButton.YesNo, MessageBoxImage.Question); 61 | if (msgResult.Equals(MessageBoxResult.Yes)) 62 | { 63 | this.nIcon.Dispose(); 64 | Log.Close(); 65 | Environment.Exit(1); 66 | } 67 | } 68 | 69 | //default page 70 | private void MainWindow_Loaded_EventHandler(object sender, RoutedEventArgs e) 71 | { 72 | this.GotoHomePage(); 73 | } 74 | 75 | //go to home page 76 | internal void GotoHomePage() 77 | { 78 | this.GotoQuickStartPage(); 79 | } 80 | 81 | //go to account setting page 82 | internal void GotoAccountPage() 83 | { 84 | this.MainHostFrame.Content = this.accountSettingPage; 85 | } 86 | 87 | internal void GotoDomainsPage() 88 | { 89 | this.MainHostFrame.Content = this.domainsSettingPage; 90 | } 91 | 92 | //go to quick start page 93 | internal void GotoQuickStartPage() 94 | { 95 | this.MainHostFrame.Content = this.quickStartPage; 96 | } 97 | 98 | //go to sync setting page 99 | internal void GotoSyncSettingPage(SyncSetting syncSetting) 100 | { 101 | this.MainHostFrame.Content = this.syncSettingPage; 102 | this.syncSettingPage.LoadSyncSetting(syncSetting); 103 | } 104 | 105 | //go to sync progress page 106 | internal void GotoSyncProgress(SyncSetting syncSetting) 107 | { 108 | this.MainHostFrame.Content = this.syncProgressPage; 109 | this.syncProgressPage.LoadSyncSetting(syncSetting); 110 | } 111 | 112 | //go to sync result page 113 | internal void GotoSyncResultPage(string jobId, TimeSpan spentTime, bool fileOverwrite, 114 | int fileSkippedCount, string fileSkippedLogPath, 115 | int fileExistsCount, string fileExistsLogPath, 116 | int fileOverwriteCount, string fileOverwriteLogPath, 117 | int fileNotOverwriteCount, string fileNotOverwriteLogPath, 118 | int fileUploadErrorCount, string fileUploadErrorLogPath, 119 | int fileUploadSuccessCount, string fileUploadSuccessLogPath) 120 | { 121 | Dispatcher.Invoke(new Action(delegate 122 | { 123 | this.MainHostFrame.Content = this.syncResultPage; 124 | this.syncResultPage.LoadSyncResult(jobId, spentTime, fileOverwrite, 125 | fileSkippedCount, fileSkippedLogPath, 126 | fileExistsCount, fileExistsLogPath, 127 | fileOverwriteCount, fileOverwriteLogPath, 128 | fileNotOverwriteCount, fileNotOverwriteLogPath, 129 | fileUploadErrorCount, fileUploadErrorLogPath, 130 | fileUploadSuccessCount, fileUploadSuccessLogPath); 131 | })); 132 | } 133 | 134 | private void MainWindow_Closing_EventHandler(object sender, System.ComponentModel.CancelEventArgs e) 135 | { 136 | e.Cancel = true; 137 | this.WindowState = WindowState.Minimized; 138 | this.ShowInTaskbar = false; 139 | this.nIcon.Visible = true; 140 | } 141 | 142 | private void NotifyIcon_MouseDoubleClick_EventHandler(object sender, MouseEventArgs e) 143 | { 144 | this.WindowState = WindowState.Normal; 145 | this.nIcon.Visible = false; 146 | this.ShowInTaskbar = true; 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /SunSync/Models/Account.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace SunSync.Models 7 | { 8 | [JsonObject(MemberSerialization.OptIn)] 9 | class Account 10 | { 11 | [JsonProperty("access_key")] 12 | public string AccessKey { set; get; } 13 | [JsonProperty("secret_key")] 14 | public string SecretKey { set; get; } 15 | 16 | /// 17 | /// load account settings from local file if exists 18 | /// 19 | /// return empty object if none 20 | /// 21 | public static Account TryLoadAccount() 22 | { 23 | Account acct = new Account(); 24 | string myDocPath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); 25 | string accPath = System.IO.Path.Combine(myDocPath, "qsunsync", "account.json"); 26 | if (File.Exists(accPath)) 27 | { 28 | string accData = ""; 29 | using (StreamReader sr = new StreamReader(accPath, Encoding.UTF8)) 30 | { 31 | accData = sr.ReadToEnd(); 32 | } 33 | try 34 | { 35 | acct = JsonConvert.DeserializeObject(accData); 36 | } 37 | catch (Exception ex) 38 | { 39 | Log.Error("parse account info failed, " + ex.Message); 40 | } 41 | } 42 | return acct; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /SunSync/Models/BucketFileInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Qiniu.Common; 3 | using Qiniu.Util; 4 | using Qiniu.RS; 5 | using Qiniu.RS.Model; 6 | using Qiniu.Http; 7 | using Newtonsoft.Json; 8 | 9 | namespace SunSync.Models 10 | { 11 | /// 12 | /// 批量获取(batch stat)的hash(可能612不存在),和本地hash进行对比,确定需要上传的文件 13 | /// 14 | public class BucketFileHash 15 | { 16 | /// 17 | /// 批量获取文件的hash 18 | /// 注意:单次请求的文件数量在1000以下 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | public static string[] BatchStat(Mac mac, string bucket, string[] keys) 25 | { 26 | string[] remoteHash = new string[keys.Length]; 27 | 28 | BucketManager bktMgr = new BucketManager(mac); 29 | 30 | int N = keys.Length; 31 | int X = 1000; 32 | int G = N / X; 33 | int M = N % X; 34 | int i; 35 | 36 | #region LOOP 37 | for (int g = 0; g < G; ++g) 38 | { 39 | string[] keys_1 = new string[X]; 40 | for (i = 0; i < X; ++i) 41 | { 42 | keys_1[i] = keys[g * X + i]; 43 | } 44 | 45 | var r1 = bktMgr.BatchStat(bucket,keys_1); 46 | 47 | for (i = 0; i < X; ++i) 48 | { 49 | var s1r = r1.Result[i]; 50 | if (s1r.Code == (int)HttpCode.OK ) 51 | { 52 | var s = JsonConvert.DeserializeObject(s1r.Data.ToString()); 53 | // FOUND 54 | remoteHash[g * X + i] = s.Hash; 55 | } 56 | } 57 | } 58 | #endregion LOOP 59 | 60 | #region RESIDUE 61 | 62 | string[] keys_2 = new string[M]; 63 | for (i = 0; i < M; ++i) 64 | { 65 | keys_2[i] = keys[G * X + i]; 66 | } 67 | 68 | var r2 = bktMgr.BatchStat(bucket, keys_2); 69 | 70 | for (i = 0; i < M; ++i) 71 | { 72 | var s2r = r2.Result[i]; 73 | 74 | if(s2r.Code == (int)HttpCode.OK) 75 | { 76 | var s = JsonConvert.DeserializeObject(s2r.Data.ToString()); 77 | // FOUND 78 | remoteHash[G * X + i] = s.Hash; 79 | } 80 | //if (r2[i].Code == 200) 81 | //{ 82 | // remoteHash[G * X + i] = r2[i].StatInfo.Hash; 83 | //} 84 | } 85 | 86 | #endregion RESIDUE 87 | 88 | return remoteHash; 89 | } 90 | 91 | } 92 | 93 | /// 94 | /// Batch请求返回的JSON格式字符串(数组) 95 | /// 以下是一个示例 96 | /// 97 | /// [ 98 | /// { 99 | /// "code":200, 100 | /// "data": 101 | /// { 102 | /// "fsize":16380, 103 | /// "hash":"FjBkn9ObUVW1Z9GvmKbbAUEp3gwE", 104 | /// "mimeType":"image/jpeg", 105 | /// "putTime":14742756456724365 106 | /// } 107 | /// }, 108 | /// { 109 | /// "code":612, 110 | /// "data": 111 | /// { 112 | /// "error":"no such file or directory" 113 | /// } 114 | /// } 115 | /// ] 116 | /// 117 | internal class StatResponse 118 | { 119 | public int CODE { get; set; } 120 | public Meta DATA { get; set; } 121 | } 122 | 123 | /// 124 | /// Stat的Data部分 125 | /// { 126 | /// "fsize":16380, 127 | /// "hash":"FjBkn9ObUVW1Z9GvmKbbAUEp3gwE", 128 | /// "mimeType":"image/jpeg", 129 | /// "putTime":14742756456724365 130 | /// } 131 | /// 132 | internal class Meta 133 | { 134 | public long fsize {get;set;} 135 | public string hash{get;set;} 136 | 137 | public string mimeType {get;set;} 138 | 139 | public long putTime{get;set;} 140 | } 141 | 142 | /// 143 | /// 待上传文件的基本信息 144 | /// 145 | public class FileItem 146 | { 147 | public string LocalFile { set; get; } 148 | public string SaveKey { set; get; } 149 | public string FileSize { get; set; } 150 | 151 | public long Length { get; set; } 152 | 153 | public string FileHash { get; set; } 154 | public string LastUpdate { get; set; } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /SunSync/Models/CachedHash.cs: -------------------------------------------------------------------------------- 1 | using Qiniu.Util; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data.SQLite; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace SunSync.Models 10 | { 11 | class CachedHash 12 | { 13 | public string LocalPath { set; get; } 14 | public string Etag { set; get; } 15 | public string LastModified { set; get; } 16 | 17 | public static void CreateCachedHashDB(string localHashDBPath) 18 | { 19 | SQLiteConnection.CreateFile(localHashDBPath); 20 | string conStr = new SQLiteConnectionStringBuilder { DataSource = localHashDBPath }.ToString(); 21 | string sqlStr = "CREATE TABLE [cached_hash] ([local_path] TEXT, [etag] CHAR(28), [last_modified] VARCHAR(50))"; 22 | using (SQLiteConnection sqlCon = new SQLiteConnection(conStr)) 23 | { 24 | sqlCon.Open(); 25 | using (SQLiteCommand sqlCmd = new SQLiteCommand(sqlStr, sqlCon)) 26 | { 27 | sqlCmd.ExecuteNonQuery(); 28 | } 29 | } 30 | } 31 | 32 | public static CachedHash GetCachedHashByLocalPath(string localPath, SQLiteConnection localHashDB) 33 | { 34 | CachedHash cachedHash = new CachedHash(); 35 | string querySql = "SELECT [etag], [last_modified] FROM [cached_hash] WHERE [local_path]=@local_path"; 36 | using (SQLiteCommand sqlCmd = new SQLiteCommand(localHashDB)) 37 | { 38 | sqlCmd.CommandText = querySql; 39 | sqlCmd.Parameters.Add("@local_path", System.Data.DbType.String); 40 | sqlCmd.Parameters["@local_path"].Value = localPath; 41 | using (SQLiteDataReader dr = sqlCmd.ExecuteReader()) 42 | { 43 | if (dr.Read()) 44 | { 45 | cachedHash.Etag = dr["etag"].ToString(); 46 | cachedHash.LastModified = dr["last_modified"].ToString(); 47 | } 48 | } 49 | } 50 | return cachedHash; 51 | } 52 | 53 | public static void UpdateCachedHash(string localPath, string etag, string lastModified, SQLiteConnection localHashDB) 54 | { 55 | string updateSql = "UPDATE [cached_hash] SET [etag]=@etag, [last_modified]=@last_modified WHERE [local_path]=@local_path"; 56 | using (SQLiteCommand sqlCmd = new SQLiteCommand(localHashDB)) 57 | { 58 | sqlCmd.CommandText = updateSql; 59 | sqlCmd.Parameters.Add("@etag", System.Data.DbType.String); 60 | sqlCmd.Parameters.Add("@last_modified", System.Data.DbType.String); 61 | sqlCmd.Parameters.Add("@local_path", System.Data.DbType.String); 62 | sqlCmd.Parameters["@etag"].Value = etag; 63 | sqlCmd.Parameters["@last_modified"].Value = lastModified; 64 | sqlCmd.Parameters["@local_path"].Value = localPath; 65 | sqlCmd.ExecuteNonQuery(); 66 | } 67 | } 68 | 69 | 70 | public static void InsertCachedHash(string localPath, string etag, string lastModified, SQLiteConnection localHashDB) 71 | { 72 | string insertSql = "INSERT INTO [cached_hash] ([local_path], [etag], [last_modified]) VALUES (@local_path, @etag, @last_modified)"; 73 | using (SQLiteCommand sqlCmd = new SQLiteCommand(insertSql, localHashDB)) 74 | { 75 | sqlCmd.CommandText = insertSql; 76 | sqlCmd.Parameters.Add("@etag", System.Data.DbType.String); 77 | sqlCmd.Parameters.Add("@last_modified", System.Data.DbType.String); 78 | sqlCmd.Parameters.Add("@local_path", System.Data.DbType.String); 79 | sqlCmd.Parameters["@etag"].Value = etag; 80 | sqlCmd.Parameters["@last_modified"].Value = lastModified; 81 | sqlCmd.Parameters["@local_path"].Value = localPath; 82 | sqlCmd.ExecuteNonQuery(); 83 | } 84 | } 85 | 86 | public static void InsertOrUpdateCachedHash(string localPath, string etag, string lastModified, SQLiteConnection localHashDB) 87 | { 88 | CachedHash cachedHash = GetCachedHashByLocalPath(localPath, localHashDB); 89 | if (!string.IsNullOrEmpty(cachedHash.LocalPath)) 90 | { 91 | UpdateCachedHash(localPath, etag, lastModified, localHashDB); 92 | } 93 | else 94 | { 95 | InsertCachedHash(localPath, etag, lastModified, localHashDB); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /SunSync/Models/Domains.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace SunSync.Models 7 | { 8 | [JsonObject(MemberSerialization.OptIn)] 9 | class Domains 10 | { 11 | [JsonProperty("up_domain")] 12 | public string UpDomain { set; get; } 13 | [JsonProperty("rs_domain")] 14 | public string RsDomain { set; get; } 15 | 16 | /// 17 | /// load domains settings from local file if exists 18 | /// 19 | /// return empty object if none 20 | /// 21 | public static Domains TryLoadDomains() 22 | { 23 | Domains domains = new Domains(); 24 | string myDocPath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); 25 | string accPath = System.IO.Path.Combine(myDocPath, "qsunsync", "domains.json"); 26 | if (File.Exists(accPath)) 27 | { 28 | string domainsData = ""; 29 | using (StreamReader sr = new StreamReader(accPath, Encoding.UTF8)) 30 | { 31 | domainsData = sr.ReadToEnd(); 32 | } 33 | try 34 | { 35 | domains = JsonConvert.DeserializeObject(domainsData); 36 | } 37 | catch (Exception ex) 38 | { 39 | Log.Error("parse domains info failed, " + ex.Message); 40 | } 41 | } 42 | return domains; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SunSync/Models/FileUploader.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Qiniu.Http; 3 | using Qiniu.Util; 4 | using System; 5 | using System.Data.SQLite; 6 | using System.IO; 7 | using Qiniu.Storage; 8 | using System.Threading; 9 | 10 | namespace SunSync.Models 11 | { 12 | class FileUploader 13 | { 14 | private SyncSetting syncSetting; 15 | private ManualResetEvent doneEvent; 16 | private SyncProgressPage syncProgressPage; 17 | private int taskId; 18 | private SQLiteConnection localHashDB; 19 | private SQLiteConnection syncLogDB; 20 | public FileUploader(SyncSetting syncSetting, ManualResetEvent doneEvent, SyncProgressPage syncProgressPage, int taskId) 21 | { 22 | this.syncSetting = syncSetting; 23 | this.doneEvent = doneEvent; 24 | this.syncProgressPage = syncProgressPage; 25 | this.taskId = taskId; 26 | this.localHashDB = syncProgressPage.LocalHashDB(); 27 | this.syncLogDB = syncProgressPage.SyncLogDB(); 28 | } 29 | 30 | public void uploadFile(object file) 31 | { 32 | if (syncProgressPage.checkCancelSignal()) 33 | { 34 | this.doneEvent.Set(); 35 | return; 36 | } 37 | string fileFullPath = file.ToString(); 38 | if (!File.Exists(fileFullPath)) 39 | { 40 | Log.Error(string.Format("file not found error, {0}", fileFullPath)); 41 | this.doneEvent.Set(); 42 | return; 43 | } 44 | //check skipped rules 45 | int fileRelativePathIndex = fileFullPath.IndexOf(this.syncSetting.SyncLocalDir); 46 | string fileRelativePath = fileFullPath.Substring(fileRelativePathIndex + this.syncSetting.SyncLocalDir.Length); 47 | if (fileRelativePath.StartsWith("\\")) 48 | { 49 | fileRelativePath = fileRelativePath.Substring(1); 50 | } 51 | 52 | string[] skippedPrefixes = this.syncSetting.SkipPrefixes.Split(','); 53 | 54 | foreach (string prefix in skippedPrefixes) 55 | { 56 | if (!string.IsNullOrWhiteSpace(prefix)) 57 | { 58 | if (fileRelativePath.StartsWith(prefix.Trim())) 59 | { 60 | //skip by prefix 61 | this.syncProgressPage.addFileSkippedLog(string.Format("{0}\t{1}", this.syncSetting.SyncTargetBucket, 62 | fileFullPath)); 63 | this.syncProgressPage.updateUploadLog("按照前缀规则跳过文件不同步 " + fileFullPath); 64 | this.syncProgressPage.updateTotalUploadProgress(); 65 | this.doneEvent.Set(); 66 | return; 67 | } 68 | } 69 | } 70 | 71 | string[] skippedSuffixes = this.syncSetting.SkipSuffixes.Split(','); 72 | foreach (string suffix in skippedSuffixes) 73 | { 74 | if (!string.IsNullOrWhiteSpace(suffix)) 75 | { 76 | if (fileRelativePath.EndsWith(suffix.Trim())) 77 | { 78 | //skip by suffix 79 | this.syncProgressPage.addFileSkippedLog(string.Format("{0}\t{1}", this.syncSetting.SyncTargetBucket, 80 | fileFullPath)); 81 | this.syncProgressPage.updateUploadLog("按照后缀规则跳过文件不同步 " + fileFullPath); 82 | this.syncProgressPage.updateTotalUploadProgress(); 83 | this.doneEvent.Set(); 84 | return; 85 | } 86 | } 87 | } 88 | 89 | //generate the file key 90 | string fileKey = ""; 91 | if (this.syncSetting.IgnoreDir) 92 | { 93 | //check ignore dir 94 | fileKey = System.IO.Path.GetFileName(fileFullPath); 95 | } 96 | else 97 | { 98 | string newFileFullPath = fileFullPath.Replace('\\', '/'); 99 | string newLocalSyncDir = this.syncSetting.SyncLocalDir.Replace('\\', '/'); 100 | int fileKeyIndex = newFileFullPath.IndexOf(newLocalSyncDir); 101 | fileKey = newFileFullPath.Substring(fileKeyIndex + newLocalSyncDir.Length); 102 | if (fileKey.StartsWith("/")) 103 | { 104 | fileKey = fileKey.Substring(1); 105 | } 106 | } 107 | 108 | //add prefix 109 | fileKey = this.syncSetting.SyncPrefix + fileKey; 110 | 111 | //set upload params 112 | 113 | string myDocPath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); 114 | string recordPath = System.IO.Path.Combine(myDocPath, "qsunsync", "resume"); 115 | if (!Directory.Exists(recordPath)) 116 | { 117 | Directory.CreateDirectory(recordPath); 118 | } 119 | 120 | bool overwriteUpload = false; 121 | bool uploadByCdn = true; 122 | switch (this.syncSetting.UploadEntryDomain) 123 | { 124 | case 1: 125 | uploadByCdn = false; break; 126 | default: 127 | uploadByCdn = true; break; 128 | } 129 | Mac mac = new Mac(SystemConfig.ACCESS_KEY, SystemConfig.SECRET_KEY); 130 | //set bucket manager and upload manager config 131 | Config config = new Config(); 132 | config.UseCdnDomains = uploadByCdn; 133 | config.PutThreshold = this.syncSetting.ChunkUploadThreshold; 134 | if (!string.IsNullOrEmpty(SystemConfig.UP_DOMAIN) && !string.IsNullOrEmpty(SystemConfig.RS_DOMAIN)) 135 | { 136 | config.Zone = new Zone 137 | { 138 | RsHost = SystemConfig.RS_DOMAIN, 139 | SrcUpHosts = new string[] { SystemConfig.UP_DOMAIN }, 140 | CdnUpHosts = new string[] { SystemConfig.UP_DOMAIN } 141 | }; 142 | } 143 | 144 | //current file info 145 | System.IO.FileInfo fileInfo = new System.IO.FileInfo(fileFullPath); 146 | long fileLength = fileInfo.Length; 147 | string fileLastModified = fileInfo.LastWriteTimeUtc.ToFileTime().ToString(); 148 | //support resume upload 149 | string recorderKey = string.Format("{0}:{1}:{2}:{3}:{4}", this.syncSetting.SyncLocalDir, 150 | this.syncSetting.SyncTargetBucket, fileKey, fileFullPath, fileLastModified); 151 | recorderKey = Tools.md5Hash(recorderKey); 152 | 153 | if (syncSetting.CheckRemoteDuplicate) 154 | { 155 | //check remotely 156 | BucketManager bucketManager = new BucketManager(mac, config); 157 | StatResult statResult = bucketManager.Stat(this.syncSetting.SyncTargetBucket, fileKey); 158 | 159 | if (statResult.Result != null && !string.IsNullOrEmpty(statResult.Result.Hash)) 160 | { 161 | //file exists in bucket 162 | string localHash = ""; 163 | //cached file info 164 | try 165 | { 166 | CachedHash cachedHash = CachedHash.GetCachedHashByLocalPath(fileFullPath, localHashDB); 167 | string cachedEtag = cachedHash.Etag; 168 | string cachedLmd = cachedHash.LastModified; 169 | if (!string.IsNullOrEmpty(cachedEtag) && !string.IsNullOrEmpty(cachedLmd)) 170 | { 171 | if (cachedLmd.Equals(fileLastModified)) 172 | { 173 | //file not modified 174 | localHash = cachedEtag; 175 | } 176 | else 177 | { 178 | //file modified, calc the hash and update db 179 | string newEtag = Qiniu.Util.ETag.CalcHash(fileFullPath); 180 | localHash = newEtag; 181 | try 182 | { 183 | CachedHash.UpdateCachedHash(fileFullPath, newEtag, fileLastModified, localHashDB); 184 | } 185 | catch (Exception ex) 186 | { 187 | Log.Error(string.Format("update local hash failed {0}", ex.Message)); 188 | } 189 | } 190 | } 191 | else 192 | { 193 | //no record, calc hash and insert into db 194 | string newEtag = Qiniu.Util.ETag.CalcHash(fileFullPath); 195 | localHash = newEtag; 196 | try 197 | { 198 | CachedHash.InsertCachedHash(fileFullPath, newEtag, fileLastModified, localHashDB); 199 | } 200 | catch (Exception ex) 201 | { 202 | Log.Error(string.Format("insert local hash failed {0}", ex.Message)); 203 | } 204 | } 205 | } 206 | catch (Exception ex) 207 | { 208 | Log.Error(string.Format("get hash from local db failed {0}", ex.Message)); 209 | localHash = Qiniu.Util.ETag.CalcHash(fileFullPath); 210 | } 211 | 212 | if (localHash.Equals(statResult.Result.Hash)) 213 | { 214 | //same file, no need to upload 215 | this.syncProgressPage.addFileExistsLog(string.Format("{0}\t{1}\t{2}", this.syncSetting.SyncTargetBucket, 216 | fileFullPath, fileKey)); 217 | this.syncProgressPage.updateUploadLog("空间已存在,跳过文件 " + fileFullPath); 218 | this.syncProgressPage.updateTotalUploadProgress(); 219 | 220 | //compatible, insert or update sync log for file 221 | try 222 | { 223 | SyncLog.InsertOrUpdateSyncLog(fileKey, fileFullPath, fileLastModified, this.syncLogDB); 224 | } 225 | catch (Exception ex) 226 | { 227 | Log.Error(string.Format("insert ot update sync log error {0}", ex.Message)); 228 | } 229 | 230 | this.doneEvent.Set(); 231 | return; 232 | } 233 | else 234 | { 235 | if (this.syncSetting.OverwriteFile) 236 | { 237 | overwriteUpload = true; 238 | this.syncProgressPage.updateUploadLog("空间已存在,将覆盖 " + fileFullPath); 239 | } 240 | else 241 | { 242 | this.syncProgressPage.updateUploadLog("空间已存在,不覆盖 " + fileFullPath); 243 | this.syncProgressPage.addFileNotOverwriteLog(string.Format("{0}\t{1}\t{2}", this.syncSetting.SyncTargetBucket, 244 | fileFullPath, fileKey)); 245 | this.syncProgressPage.updateTotalUploadProgress(); 246 | this.doneEvent.Set(); 247 | return; 248 | } 249 | } 250 | } 251 | } 252 | else 253 | { 254 | //check locally 255 | try 256 | { 257 | SyncLog syncLog = SyncLog.GetSyncLogByKey(fileKey, this.syncLogDB); 258 | if (!string.IsNullOrEmpty(syncLog.Key)) 259 | { 260 | //has sync log and check whether it changes 261 | if (syncLog.LocalPath.Equals(fileFullPath) && syncLog.LastModified.Equals(fileLastModified)) 262 | { 263 | this.syncProgressPage.addFileExistsLog(string.Format("{0}\t{1}\t{2}", this.syncSetting.SyncTargetBucket, 264 | fileFullPath, fileKey)); 265 | this.syncProgressPage.updateUploadLog("本地检查已同步,跳过" + fileFullPath); 266 | this.syncProgressPage.updateTotalUploadProgress(); 267 | this.doneEvent.Set(); 268 | return; 269 | } 270 | } 271 | } 272 | catch (Exception ex) 273 | { 274 | Log.Error(string.Format("get sync log failed {0}", ex.Message)); 275 | this.doneEvent.Set(); 276 | return; 277 | } 278 | 279 | if (this.syncSetting.OverwriteFile) 280 | { 281 | overwriteUpload = true; 282 | } 283 | } 284 | 285 | //if file not exists or need to overwrite 286 | this.syncProgressPage.updateUploadLog("准备上传文件 " + fileFullPath); 287 | string resumeFile = System.IO.Path.Combine(recordPath, recorderKey); 288 | 289 | UploadManager uploadManager = new UploadManager(config); 290 | 291 | PutExtra putExtra = new PutExtra(); 292 | putExtra.ResumeRecordFile = resumeFile; 293 | putExtra.BlockUploadThreads = this.syncSetting.DefaultChunkSize; 294 | 295 | UploadProgressHandler progressHandler = new UploadProgressHandler(delegate (long uploadBytes, long totalBytes) 296 | { 297 | Console.WriteLine("progress: " + uploadBytes + ", " + totalBytes); 298 | double percent = uploadBytes * 1.0 / totalBytes; 299 | this.syncProgressPage.updateSingleFileProgress(taskId, fileFullPath, fileKey, fileLength, percent); 300 | }); 301 | putExtra.ProgressHandler = progressHandler; 302 | putExtra.UploadController = new UploadController(delegate 303 | { 304 | if (this.syncProgressPage.FinishSignal) 305 | { 306 | return UploadControllerAction.Aborted; 307 | } 308 | else 309 | { 310 | 311 | if (this.syncProgressPage.CancelSignal) 312 | { 313 | return UploadControllerAction.Suspended; 314 | } 315 | else 316 | { 317 | return UploadControllerAction.Activated; 318 | } 319 | } 320 | }); 321 | 322 | PutPolicy putPolicy = new PutPolicy(); 323 | if (overwriteUpload) 324 | { 325 | putPolicy.Scope = this.syncSetting.SyncTargetBucket + ":" + fileKey; 326 | } 327 | else 328 | { 329 | putPolicy.Scope = this.syncSetting.SyncTargetBucket; 330 | } 331 | putPolicy.SetExpires(24 * 30 * 3600); 332 | //set file type 333 | putPolicy.FileType = this.syncSetting.FileType; 334 | Auth auth = new Auth(mac); 335 | string uptoken = auth.CreateUploadToken(putPolicy.ToJsonString()); 336 | 337 | this.syncProgressPage.updateUploadLog("开始上传文件 " + fileFullPath); 338 | 339 | HttpResult uploadResult = uploadManager.UploadFile(fileFullPath, fileKey, uptoken, putExtra); 340 | 341 | if (uploadResult.Code != 200) 342 | { 343 | this.syncProgressPage.updateUploadLog("上传失败 " + fileFullPath + "," + uploadResult.RefText); 344 | this.syncProgressPage.addFileUploadErrorLog(string.Format("{0}\t{1}\t{2}\t{3}", this.syncSetting.SyncTargetBucket, 345 | fileFullPath, fileKey, uploadResult.Text + "" + uploadResult.RefText)); 346 | 347 | //file exists error 348 | if (uploadResult.Code == 614) 349 | { 350 | this.syncProgressPage.updateUploadLog("空间已存在,未覆盖 " + fileFullPath); 351 | this.syncProgressPage.addFileNotOverwriteLog(string.Format("{0}\t{1}\t{2}", this.syncSetting.SyncTargetBucket, 352 | fileFullPath, fileKey)); 353 | } 354 | } 355 | else 356 | { 357 | //insert or update sync log for file 358 | try 359 | { 360 | SyncLog.InsertOrUpdateSyncLog(fileKey, fileFullPath, fileLastModified, this.syncLogDB); 361 | } 362 | catch (Exception ex) 363 | { 364 | Log.Error(string.Format("insert ot update sync log error {0}", ex.Message)); 365 | } 366 | 367 | //write new file hash to local db 368 | if (!overwriteUpload) 369 | { 370 | PutRet putRet = JsonConvert.DeserializeObject(uploadResult.Text); 371 | string fileHash = putRet.Hash; 372 | if (this.localHashDB != null) 373 | { 374 | try 375 | { 376 | CachedHash.InsertOrUpdateCachedHash(fileFullPath, fileHash, fileLastModified, this.localHashDB); 377 | } 378 | catch (Exception ex) 379 | { 380 | Log.Error(string.Format("insert or update cached hash error {0}", ex.Message)); 381 | } 382 | } 383 | Log.Debug(string.Format("insert or update qiniu hash to local: '{0}' => '{1}'", fileFullPath, fileHash)); 384 | } 385 | 386 | //update 387 | if (overwriteUpload) 388 | { 389 | this.syncProgressPage.addFileOverwriteLog(string.Format("{0}\t{1}\t{2}", this.syncSetting.SyncTargetBucket, 390 | fileFullPath, fileKey)); 391 | } 392 | this.syncProgressPage.updateUploadLog("上传成功 " + fileFullPath); 393 | this.syncProgressPage.addFileUploadSuccessLog(string.Format("{0}\t{1}\t{2}", this.syncSetting.SyncTargetBucket, 394 | fileFullPath, fileKey)); 395 | this.syncProgressPage.updateTotalUploadProgress(); 396 | } 397 | Log.Info("upload finish for " + fileFullPath + ", " + uploadResult.RefText); 398 | this.doneEvent.Set(); 399 | return; 400 | 401 | } 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /SunSync/Models/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace SunSync.Models 8 | { 9 | class Log 10 | { 11 | private static TraceSource logSource = new TraceSource("QSunSync"); 12 | public static void Init() 13 | { 14 | string myDocPath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); 15 | string syncLogPath = System.IO.Path.Combine(myDocPath, "qsunsync", "sync.log"); 16 | 17 | logSource.Switch = new SourceSwitch("logSwitch"); 18 | logSource.Switch.Level = SourceLevels.All; 19 | logSource.Listeners.Remove("Default"); 20 | 21 | TextWriterTraceListener prodListener = new TextWriterTraceListener(syncLogPath); 22 | prodListener.Filter = new EventTypeFilter(SourceLevels.Information); 23 | prodListener.TraceOutputOptions = TraceOptions.None; 24 | logSource.Listeners.Add(prodListener); 25 | 26 | /* 27 | ConsoleTraceListener devListener = new ConsoleTraceListener(); 28 | devListener.Filter = new EventTypeFilter(SourceLevels.Verbose); 29 | logSource.Listeners.Add(devListener); 30 | */ 31 | } 32 | 33 | public static void Debug(string message) 34 | { 35 | string timestamp = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 36 | logSource.TraceEvent(TraceEventType.Verbose, 4, string.Format("[{0}] {1}", timestamp, message)); 37 | logSource.Flush(); 38 | } 39 | 40 | public static void Info(string message) 41 | { 42 | string timestamp = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 43 | logSource.TraceEvent(TraceEventType.Information, 3, string.Format("[{0}] {1}", timestamp, message)); 44 | logSource.Flush(); 45 | } 46 | 47 | public static void Warn(string message) 48 | { 49 | string timestamp = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 50 | logSource.TraceEvent(TraceEventType.Warning, 2, string.Format("[{0}] {1}", timestamp, message)); 51 | logSource.Flush(); 52 | } 53 | 54 | public static void Error(string message) 55 | { 56 | string timestamp = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 57 | logSource.TraceEvent(TraceEventType.Error, 1, string.Format("[{0}] {1}", timestamp, message)); 58 | logSource.Flush(); 59 | } 60 | 61 | public static void Fatal(string message) 62 | { 63 | string timestamp = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 64 | logSource.TraceEvent(TraceEventType.Critical, 0, string.Format("[{0}] {1}", timestamp, message)); 65 | logSource.Flush(); 66 | } 67 | 68 | public static void Close() 69 | { 70 | if (logSource != null) 71 | { 72 | logSource.Close(); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SunSync/Models/LogExporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Windows.Forms; 7 | 8 | namespace SunSync.Models 9 | { 10 | class LogExporter 11 | { 12 | public static void exportLog(string fileUploadSuccessLogPath, 13 | string fileUploadErrorLogPath, 14 | string fileSkippedLogPath, 15 | string fileExistsLogPath, 16 | string fileNotOverwriteLogPath, 17 | string fileOverwriteLogPath, string logSaveFilePath) 18 | { 19 | int encodingSize = 3; 20 | try 21 | { 22 | using (StreamWriter sw = new StreamWriter(logSaveFilePath, false, Encoding.UTF8)) 23 | { 24 | string line = null; 25 | 26 | FileInfo fi = new FileInfo(fileUploadSuccessLogPath); 27 | if (fi.Length > encodingSize) 28 | { 29 | try 30 | { 31 | sw.WriteLine("本次同步成功文件列表:"); 32 | using (StreamReader isr = new StreamReader(fileUploadSuccessLogPath, Encoding.UTF8)) 33 | { 34 | while ((line = isr.ReadLine()) != null) 35 | { 36 | sw.WriteLine(line); 37 | } 38 | } 39 | sw.WriteLine(); 40 | } 41 | catch (Exception ex) 42 | { 43 | Log.Error(string.Format("export success file list error {0}", ex.Message)); 44 | } 45 | } 46 | 47 | fi = new FileInfo(fileUploadErrorLogPath); 48 | if (fi.Length > encodingSize) 49 | { 50 | try 51 | { 52 | sw.WriteLine("本次同步失败文件列表:"); 53 | using (StreamReader isr = new StreamReader(fileUploadErrorLogPath, Encoding.UTF8)) 54 | { 55 | while ((line = isr.ReadLine()) != null) 56 | { 57 | sw.WriteLine(line); 58 | } 59 | } 60 | sw.WriteLine(); 61 | } 62 | catch (Exception ex) 63 | { 64 | Log.Error(string.Format("export failed file list error {0}", ex.Message)); 65 | } 66 | } 67 | 68 | fi = new FileInfo(fileSkippedLogPath); 69 | if (fi.Length > encodingSize) 70 | { 71 | try 72 | { 73 | sw.WriteLine("按照前缀或后缀规则跳过不进行同步的文件列表:"); 74 | using (StreamReader isr = new StreamReader(fileSkippedLogPath, Encoding.UTF8)) 75 | { 76 | while ((line = isr.ReadLine()) != null) 77 | { 78 | sw.WriteLine(line); 79 | } 80 | } 81 | sw.WriteLine(); 82 | } 83 | catch (Exception ex) 84 | { 85 | Log.Error(string.Format("export skipped file list error {0}", ex.Message)); 86 | } 87 | } 88 | 89 | fi = new FileInfo(fileExistsLogPath); 90 | if (fi.Length > encodingSize) 91 | { 92 | try 93 | { 94 | sw.WriteLine("远程已存在,且本地未发生变化的文件列表:"); 95 | using (StreamReader isr = new StreamReader(fileExistsLogPath, Encoding.UTF8)) 96 | { 97 | while ((line = isr.ReadLine()) != null) 98 | { 99 | sw.WriteLine(line); 100 | } 101 | } 102 | sw.WriteLine(); 103 | } 104 | catch (Exception ex) 105 | { 106 | Log.Error(string.Format("export exists file list error {0}", ex.Message)); 107 | } 108 | } 109 | 110 | fi = new FileInfo(fileNotOverwriteLogPath); 111 | if (fi.Length > encodingSize) 112 | { 113 | try 114 | { 115 | sw.WriteLine("本地文件发生改动,但远程未覆盖的文件列表:"); 116 | using (StreamReader isr = new StreamReader(fileNotOverwriteLogPath, Encoding.UTF8)) 117 | { 118 | while ((line = isr.ReadLine()) != null) 119 | { 120 | sw.WriteLine(line); 121 | } 122 | } 123 | sw.WriteLine(); 124 | } 125 | catch (Exception ex) 126 | { 127 | Log.Error(string.Format("export not overwrite file list error {0}", ex.Message)); 128 | } 129 | } 130 | 131 | fi = new FileInfo(fileOverwriteLogPath); 132 | if (fi.Length > encodingSize) 133 | { 134 | try 135 | { 136 | sw.WriteLine("本地文件发生改动,远程强制覆盖的文件列表:"); 137 | using (StreamReader isr = new StreamReader(fileOverwriteLogPath, Encoding.UTF8)) 138 | { 139 | while ((line = isr.ReadLine()) != null) 140 | { 141 | sw.WriteLine(line); 142 | } 143 | } 144 | sw.WriteLine(); 145 | } 146 | catch (Exception ex) 147 | { 148 | Log.Error(string.Format("export overwrite file list error {0}", ex.Message)); 149 | } 150 | 151 | } 152 | 153 | sw.Flush(); 154 | } 155 | } 156 | catch (Exception ex) 157 | { 158 | Log.Error(string.Format("export log failed due to {0}", ex.Message)); 159 | MessageBox.Show("导出日志失败," + ex.Message, "导出日志", MessageBoxButtons.OK, MessageBoxIcon.Error); 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /SunSync/Models/PutRet.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace SunSync.Models 4 | { 5 | [JsonObject(MemberSerialization.OptIn)] 6 | class PutRet 7 | { 8 | [JsonProperty("hash")] 9 | public string Hash { set; get; } 10 | [JsonProperty("key")] 11 | public string Key { set; get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SunSync/Models/SyncLog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SQLite; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace SunSync.Models 8 | { 9 | class SyncLog 10 | { 11 | //@TODO make key unique index 12 | public string Key { set; get; } 13 | public string LocalPath { set; get; } 14 | public string LastModified { set; get; } 15 | 16 | public static void CreateSyncLogDB(string syncLogDBPath) 17 | { 18 | SQLiteConnection.CreateFile(syncLogDBPath); 19 | string conStr = new SQLiteConnectionStringBuilder { DataSource = syncLogDBPath }.ToString(); 20 | string sqlStr = "CREATE TABLE [sync_log] ([key] TEXT, [local_path] TEXT, [last_modified] VARCHAR(50))"; 21 | using (SQLiteConnection sqlCon = new SQLiteConnection(conStr)) 22 | { 23 | sqlCon.Open(); 24 | using (SQLiteCommand sqlCmd = new SQLiteCommand(sqlStr, sqlCon)) 25 | { 26 | sqlCmd.ExecuteNonQuery(); 27 | } 28 | } 29 | } 30 | 31 | public static SyncLog GetSyncLogByKey(string key, SQLiteConnection syncLogDB) 32 | { 33 | SyncLog syncLog = new SyncLog(); 34 | string querySql = "SELECT [key], [local_path], [last_modified] FROM [sync_log] WHERE [key]=@key"; 35 | using (SQLiteCommand sqlCmd = new SQLiteCommand(syncLogDB)) 36 | { 37 | sqlCmd.CommandText = querySql; 38 | sqlCmd.Parameters.Add("@key", System.Data.DbType.String); 39 | sqlCmd.Parameters["@key"].Value = key; 40 | using (SQLiteDataReader dr = sqlCmd.ExecuteReader()) 41 | { 42 | if (dr.Read()) 43 | { 44 | syncLog.Key = dr["key"].ToString(); 45 | syncLog.LocalPath = dr["local_path"].ToString(); 46 | syncLog.LastModified = dr["last_modified"].ToString(); 47 | } 48 | } 49 | } 50 | return syncLog; 51 | } 52 | 53 | public static void UpdateSyncLog(string key, string localPath, string lastModified, SQLiteConnection syncLogDB) 54 | { 55 | string updateSql = "UPDATE [sync_log] SET [local_path]=@local_path, [last_modified]=@last_modified WHERE [key]=@key"; 56 | using (SQLiteCommand sqlCmd = new SQLiteCommand(syncLogDB)) 57 | { 58 | sqlCmd.CommandText = updateSql; 59 | sqlCmd.Parameters.Add("@local_path", System.Data.DbType.String); 60 | sqlCmd.Parameters.Add("@last_modified", System.Data.DbType.String); 61 | sqlCmd.Parameters.Add("@key", System.Data.DbType.String); 62 | sqlCmd.Parameters["@local_path"].Value = localPath; 63 | sqlCmd.Parameters["@last_modified"].Value = lastModified; 64 | sqlCmd.Parameters["@key"].Value = key; 65 | sqlCmd.ExecuteNonQuery(); 66 | } 67 | } 68 | 69 | public static void InsertSyncLog(string key, string localPath, string lastModified, SQLiteConnection syncLogDB) 70 | { 71 | string insertSql = "INSERT INTO [sync_log] ([key], [local_path], [last_modified]) VALUES (@key, @local_path, @last_modified)"; 72 | using (SQLiteCommand sqlCmd = new SQLiteCommand(syncLogDB)) 73 | { 74 | sqlCmd.CommandText = insertSql; 75 | sqlCmd.Parameters.Add("@key", System.Data.DbType.String); 76 | sqlCmd.Parameters.Add("@local_path", System.Data.DbType.String); 77 | sqlCmd.Parameters.Add("@last_modified", System.Data.DbType.String); 78 | sqlCmd.Parameters["@key"].Value = key; 79 | sqlCmd.Parameters["@local_path"].Value = localPath; 80 | sqlCmd.Parameters["@last_modified"].Value = lastModified; 81 | sqlCmd.ExecuteNonQuery(); 82 | } 83 | } 84 | 85 | public static void InsertOrUpdateSyncLog(string key, string localPath, string lastModified, SQLiteConnection syncLogDB) 86 | { 87 | SyncLog syncLog = GetSyncLogByKey(key, syncLogDB); 88 | if (!string.IsNullOrEmpty(syncLog.Key)) 89 | { 90 | UpdateSyncLog(key, localPath, lastModified, syncLogDB); 91 | } 92 | else 93 | { 94 | InsertSyncLog(key, localPath, lastModified, syncLogDB); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SunSync/Models/SyncRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SQLite; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace SunSync.Models 8 | { 9 | /// 10 | /// sync job record in database 11 | /// 12 | public class SyncRecord 13 | { 14 | public string SyncId { set; get; } 15 | public string SyncLocalDir { set; get; } 16 | public string SyncTargetBucket { set; get; } 17 | public int FileType { set; get; } 18 | public bool CheckRemoteDuplicate { set; get; } 19 | public string SyncPrefix { set; get; } 20 | public bool CheckNewFiles { set; get; } 21 | public bool IgnoreDir { set; get; } 22 | public string SkipPrefixes { set; get; } 23 | public string SkipSuffixes { set; get; } 24 | public bool OverwriteFile { set; get; } 25 | public int DefaultChunkSize { set; get; } 26 | public int ChunkUploadThreshold { set; get; } 27 | public int SyncThreadCount { set; get; } 28 | public int UploadEntryDomain { set; get; } 29 | public DateTime SyncDateTime { set; get; } 30 | //for display 31 | public string SyncDateTimeStr { set; get; } 32 | 33 | public static void CreateSyncRecordDB(string jobsDbPath) 34 | { 35 | //create database 36 | SQLiteConnection.CreateFile(jobsDbPath); 37 | 38 | //create table 39 | string sqlCreate = new StringBuilder() 40 | .Append("CREATE TABLE [sync_jobs]") 41 | .Append("([sync_id] CHAR(32) UNIQUE NOT NULL, ") 42 | .Append("[sync_local_dir] VARCHAR(255) NOT NULL,") 43 | .Append("[sync_target_bucket] VARCHAR(64) NOT NULL,") 44 | .Append("[file_type] INTEGER NOT NULL,") 45 | .Append("[check_remote_duplicate] BOOLEAN NOT NULL,") 46 | .Append("[sync_prefix] VARCHAR(255) NOT NULL,") 47 | .Append("[check_new_files] BOOLEAN NOT NULL,") 48 | .Append("[ignore_dir] BOOLEAN NOT NULL,") 49 | .Append("[skip_prefixes] VARCHAR(500) NOT NULL,") 50 | .Append("[skip_suffixes] VARCHAR(500) NOT NULL,") 51 | .Append("[overwrite_file] BOOLEAN NOT NULL,") 52 | .Append("[default_chunk_size] INTEGER NOT NULL,") 53 | .Append("[chunk_upload_threshold] INTEGER NOT NULL,") 54 | .Append("[sync_thread_count] INTEGER NOT NULL,") 55 | .Append("[upload_entry_domain] INTEGER NOT NULL,") 56 | .Append("[sync_date_time] DATE NOT NULL )").ToString(); 57 | string conStr = new SQLiteConnectionStringBuilder { DataSource = jobsDbPath }.ToString(); 58 | using (SQLiteConnection sqlCon = new SQLiteConnection(conStr)) 59 | { 60 | sqlCon.Open(); 61 | using (SQLiteCommand sqlCmd = new SQLiteCommand(sqlCon)) 62 | { 63 | sqlCmd.CommandText = sqlCreate; 64 | sqlCmd.ExecuteNonQuery(); 65 | } 66 | } 67 | } 68 | 69 | public static void DeleteSyncJobById(string syncId, string jobsDbPath) 70 | { 71 | string conStr = new SQLiteConnectionStringBuilder { DataSource = jobsDbPath }.ToString(); 72 | string deleteSql = string.Format("DELETE FROM [sync_jobs] WHERE [sync_id]='{0}'", syncId); 73 | using (SQLiteConnection sqlCon = new SQLiteConnection(conStr)) 74 | { 75 | sqlCon.Open(); 76 | using (SQLiteCommand sqlCmd = new SQLiteCommand(sqlCon)) 77 | { 78 | sqlCmd.CommandText = deleteSql; 79 | sqlCmd.ExecuteNonQuery(); 80 | } 81 | } 82 | } 83 | 84 | public static List LoadRecentSyncJobs(string jobsDbPath) 85 | { 86 | List syncRecords = new List(); 87 | 88 | string conStr = new SQLiteConnectionStringBuilder { DataSource = jobsDbPath }.ToString(); 89 | string queryStr = "SELECT * FROM [sync_jobs] ORDER BY [sync_date_time] DESC"; 90 | using (SQLiteConnection sqlCon = new SQLiteConnection(conStr)) 91 | { 92 | sqlCon.Open(); 93 | using (SQLiteCommand sqlCmd = new SQLiteCommand(queryStr, sqlCon)) 94 | { 95 | using (SQLiteDataReader dr = sqlCmd.ExecuteReader()) 96 | { 97 | while (dr.Read()) 98 | { 99 | SyncRecord record = new SyncRecord(); 100 | record.SyncId = Convert.ToString(dr["sync_id"]); 101 | record.SyncLocalDir = Convert.ToString(dr["sync_local_dir"]); 102 | record.FileType=Convert.ToInt32(dr["file_type"]); 103 | record.CheckRemoteDuplicate = Convert.ToBoolean(dr["check_remote_duplicate"]); 104 | record.SyncTargetBucket = Convert.ToString(dr["sync_target_bucket"]); 105 | record.SyncPrefix = Convert.ToString(dr["sync_prefix"]); 106 | record.CheckNewFiles = Convert.ToBoolean(dr["check_new_files"]); 107 | record.IgnoreDir = Convert.ToBoolean(dr["ignore_dir"]); 108 | record.SkipPrefixes = Convert.ToString(dr["skip_prefixes"]); 109 | record.SkipSuffixes = Convert.ToString(dr["skip_suffixes"]); 110 | record.OverwriteFile = Convert.ToBoolean(dr["overwrite_file"]); 111 | record.DefaultChunkSize = Convert.ToInt32(dr["default_chunk_size"]); 112 | record.ChunkUploadThreshold = Convert.ToInt32(dr["chunk_upload_threshold"]); 113 | record.SyncThreadCount = Convert.ToInt32(dr["sync_thread_count"]); 114 | record.UploadEntryDomain = Convert.ToInt32(dr["upload_entry_domain"]); 115 | record.SyncDateTime = Convert.ToDateTime(dr["sync_date_time"]); 116 | record.SyncDateTimeStr = record.SyncDateTime.ToString("yyyy-MM-dd HH:mm:ss"); 117 | syncRecords.Add(record); 118 | } 119 | } 120 | } 121 | } 122 | 123 | return syncRecords; 124 | } 125 | 126 | public static void RecordSyncJob(string syncId, DateTime syncDateTime, SyncSetting syncSetting, string jobsDbPath) 127 | { 128 | string conStr = new SQLiteConnectionStringBuilder { DataSource = jobsDbPath }.ToString(); 129 | string queryDelete = "DELETE FROM [sync_jobs] WHERE [sync_id]=@sync_id"; 130 | string queryInsert = new StringBuilder().Append("INSERT INTO [sync_jobs] ([sync_id], [sync_local_dir], [sync_target_bucket], [file_type], [check_remote_duplicate], ") 131 | .Append("[sync_prefix], [check_new_files], [ignore_dir], [skip_prefixes], [skip_suffixes], [overwrite_file], [default_chunk_size], [chunk_upload_threshold], [sync_thread_count], ") 132 | .Append("[upload_entry_domain], [sync_date_time]) VALUES ( @sync_id, @sync_local_dir, @sync_target_bucket, @file_type, @check_remote_duplicate, @sync_prefix, @check_new_files, @ignore_dir, ") 133 | .Append("@skip_prefixes, @skip_suffixes, @overwrite_file, @default_chunk_size, @chunk_upload_threshold, @sync_thread_count, @upload_entry_domain, @sync_date_time)").ToString(); 134 | using (SQLiteConnection sqlCon = new SQLiteConnection(conStr)) 135 | { 136 | sqlCon.Open(); 137 | using (SQLiteTransaction sqlTrans = sqlCon.BeginTransaction()) 138 | { 139 | using (SQLiteCommand sqlCmd = new SQLiteCommand(sqlCon)) 140 | { 141 | //do delete if exists 142 | sqlCmd.CommandText = queryDelete; 143 | sqlCmd.Parameters.Add("@sync_id", System.Data.DbType.String); 144 | 145 | sqlCmd.Parameters["@sync_id"].Value = syncId; 146 | sqlCmd.ExecuteNonQuery(); 147 | 148 | //do insert 149 | sqlCmd.CommandText = queryInsert; 150 | sqlCmd.Parameters.Add("@sync_id", System.Data.DbType.String); 151 | sqlCmd.Parameters.Add("@sync_local_dir", System.Data.DbType.String); 152 | sqlCmd.Parameters.Add("@sync_target_bucket", System.Data.DbType.String); 153 | sqlCmd.Parameters.Add("@file_type",System.Data.DbType.Int32); 154 | sqlCmd.Parameters.Add("@check_remote_duplicate",System.Data.DbType.Boolean); 155 | sqlCmd.Parameters.Add("@sync_prefix", System.Data.DbType.String); 156 | sqlCmd.Parameters.Add("@check_new_files",System.Data.DbType.Boolean); 157 | sqlCmd.Parameters.Add("@ignore_dir", System.Data.DbType.Boolean); 158 | sqlCmd.Parameters.Add("@skip_prefixes",System.Data.DbType.String); 159 | sqlCmd.Parameters.Add("@skip_suffixes", System.Data.DbType.String); 160 | sqlCmd.Parameters.Add("@overwrite_file", System.Data.DbType.Boolean); 161 | sqlCmd.Parameters.Add("@default_chunk_size", System.Data.DbType.Int32); 162 | sqlCmd.Parameters.Add("@chunk_upload_threshold", System.Data.DbType.Int32); 163 | sqlCmd.Parameters.Add("@sync_thread_count", System.Data.DbType.Int32); 164 | sqlCmd.Parameters.Add("@upload_entry_domain", System.Data.DbType.String); 165 | sqlCmd.Parameters.Add("@sync_date_time", System.Data.DbType.DateTime); 166 | 167 | sqlCmd.Parameters["@sync_id"].Value = syncId; 168 | sqlCmd.Parameters["@sync_local_dir"].Value = syncSetting.SyncLocalDir; 169 | sqlCmd.Parameters["@sync_target_bucket"].Value = syncSetting.SyncTargetBucket; 170 | sqlCmd.Parameters["@file_type"].Value = syncSetting.FileType; 171 | sqlCmd.Parameters["@check_remote_duplicate"].Value = syncSetting.CheckRemoteDuplicate; 172 | sqlCmd.Parameters["@sync_prefix"].Value = syncSetting.SyncPrefix; 173 | sqlCmd.Parameters["@check_new_files"].Value = syncSetting.CheckNewFiles; 174 | sqlCmd.Parameters["@ignore_dir"].Value = syncSetting.IgnoreDir; 175 | sqlCmd.Parameters["@skip_prefixes"].Value = syncSetting.SkipPrefixes; 176 | sqlCmd.Parameters["@skip_suffixes"].Value = syncSetting.SkipSuffixes; 177 | sqlCmd.Parameters["@overwrite_file"].Value = syncSetting.OverwriteFile; 178 | sqlCmd.Parameters["@default_chunk_size"].Value = syncSetting.DefaultChunkSize; 179 | sqlCmd.Parameters["@chunk_upload_threshold"].Value = syncSetting.ChunkUploadThreshold; 180 | sqlCmd.Parameters["@sync_thread_count"].Value = syncSetting.SyncThreadCount; 181 | sqlCmd.Parameters["@upload_entry_domain"].Value = syncSetting.UploadEntryDomain; 182 | sqlCmd.Parameters["@sync_date_time"].Value = syncDateTime; 183 | 184 | sqlCmd.ExecuteNonQuery(); 185 | } 186 | 187 | //commit 188 | sqlTrans.Commit(); 189 | } 190 | } 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /SunSync/Models/SyncSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.SQLite; 3 | using System.IO; 4 | namespace SunSync.Models 5 | { 6 | public class SyncSetting 7 | { 8 | //local dir to sync 9 | public string SyncLocalDir { set; get; } 10 | //target bucket 11 | public string SyncTargetBucket { set; get; } 12 | // file type 13 | public int FileType { set; get; } 14 | //check remote duplicate 15 | public bool CheckRemoteDuplicate { set; get; } 16 | //prefix 17 | public string SyncPrefix { set; get; } 18 | //check new files 19 | public bool CheckNewFiles { set; get; } 20 | //ignore dir 21 | public bool IgnoreDir { set; get; } 22 | //skip prefixes 23 | public string SkipPrefixes { set; get; } 24 | //skip suffixes 25 | public string SkipSuffixes { set; get; } 26 | //overwrite same file 27 | public bool OverwriteFile { set; get; } 28 | //default chunk size 29 | public int DefaultChunkSize { set; get; } 30 | //upload threshold 31 | public int ChunkUploadThreshold { set; get; } 32 | //sync thread count 33 | public int SyncThreadCount { set; get; } 34 | //upload entry domain 35 | public int UploadEntryDomain { set; get; } 36 | 37 | /// 38 | /// load sync settings from the database by job id 39 | /// 40 | /// job id 41 | /// 42 | /// return null if not exist 43 | /// 44 | public static SyncSetting LoadSyncSettingByJobId(string syncId) 45 | { 46 | SyncSetting setting = null; 47 | string myDocPath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); 48 | string jobsDb = System.IO.Path.Combine(myDocPath, "qsunsync", "jobs.db"); 49 | 50 | if (File.Exists(jobsDb)) 51 | { 52 | string conStr = new SQLiteConnectionStringBuilder { DataSource = jobsDb }.ToString(); 53 | string query = "SELECT * FROM [sync_jobs] WHERE [sync_id]=@sync_id"; 54 | using (SQLiteConnection sqlCon = new SQLiteConnection(conStr)) 55 | { 56 | sqlCon.Open(); 57 | using (SQLiteCommand sqlCmd = new SQLiteCommand(sqlCon)) 58 | { 59 | sqlCmd.CommandText = query; 60 | sqlCmd.Parameters.Add("@sync_id", System.Data.DbType.String); 61 | sqlCmd.Parameters["@sync_id"].Value = syncId; 62 | using (SQLiteDataReader dr = sqlCmd.ExecuteReader()) 63 | { 64 | if (dr.Read()) 65 | { 66 | setting = new SyncSetting(); 67 | setting.SyncLocalDir = Convert.ToString(dr["sync_local_dir"]); 68 | setting.SyncTargetBucket = Convert.ToString(dr["sync_target_bucket"]); 69 | setting.FileType = Convert.ToInt32(dr["file_type"]); 70 | setting.CheckRemoteDuplicate = Convert.ToBoolean(dr["check_remote_duplicate"]); 71 | setting.SyncPrefix = Convert.ToString(dr["sync_prefix"]); 72 | setting.CheckNewFiles = Convert.ToBoolean(dr["check_new_files"]); 73 | setting.IgnoreDir = Convert.ToBoolean(dr["ignore_dir"]); 74 | setting.SkipPrefixes = Convert.ToString(dr["skip_prefixes"]); 75 | setting.SkipSuffixes = Convert.ToString(dr["skip_suffixes"]); 76 | setting.OverwriteFile = Convert.ToBoolean(dr["overwrite_file"]); 77 | setting.DefaultChunkSize = Convert.ToInt32(dr["default_chunk_size"]); 78 | setting.ChunkUploadThreshold = Convert.ToInt32(dr["chunk_upload_threshold"]); 79 | setting.SyncThreadCount = Convert.ToInt32(dr["sync_thread_count"]); 80 | setting.UploadEntryDomain = Convert.ToInt32(dr["upload_entry_domain"]); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | return setting; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /SunSync/Models/SystemConfig.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace SunSync.Models 3 | { 4 | class SystemConfig 5 | { 6 | public static string ACCESS_KEY; 7 | public static string SECRET_KEY; 8 | public static string UP_DOMAIN; 9 | public static string RS_DOMAIN; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SunSync/Models/Tools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace SunSync.Models 6 | { 7 | class Tools 8 | { 9 | /// 10 | /// md5 hash in hex string 11 | /// 12 | /// str to hash 13 | /// str hashed and format in hex string 14 | public static string md5Hash(string str) 15 | { 16 | MD5 md5 = new MD5CryptoServiceProvider(); 17 | byte[] data = Encoding.UTF8.GetBytes(str); 18 | byte[] hashData = md5.ComputeHash(data); 19 | StringBuilder sb = new StringBuilder(hashData.Length * 2); 20 | foreach (byte b in hashData) 21 | { 22 | sb.AppendFormat("{0:x2}", b); 23 | } 24 | return sb.ToString(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SunSync/Models/UploadInfo.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace SunSync.Models 3 | { 4 | class UploadInfo 5 | { 6 | public string LocalPath { set; get; } 7 | public string FileKey { set; get; } 8 | public string Progress { set; get; } 9 | public string Speed { set; get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SunSync/Pictures/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/back.png -------------------------------------------------------------------------------- /SunSync/Pictures/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/cloud.png -------------------------------------------------------------------------------- /SunSync/Pictures/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/folder.png -------------------------------------------------------------------------------- /SunSync/Pictures/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/home.png -------------------------------------------------------------------------------- /SunSync/Pictures/qiniu_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/qiniu_logo.jpg -------------------------------------------------------------------------------- /SunSync/Pictures/reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/reload.png -------------------------------------------------------------------------------- /SunSync/Pictures/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/result.png -------------------------------------------------------------------------------- /SunSync/Pictures/sun_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/sun_logo.jpg -------------------------------------------------------------------------------- /SunSync/Pictures/sunsync.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/Pictures/sunsync.ico -------------------------------------------------------------------------------- /SunSync/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Windows; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("QSunSync")] 11 | [assembly: AssemblyDescription("七牛云文件同步工具")] 12 | [assembly: AssemblyConfiguration("")] 13 | [assembly: AssemblyCompany("上海七牛信息技术有限公司")] 14 | [assembly: AssemblyProduct("QSunSync")] 15 | [assembly: AssemblyCopyright("Copyright © 2018")] 16 | [assembly: AssemblyTrademark("QSunSync")] 17 | [assembly: AssemblyCulture("")] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [assembly: ComVisible(false)] 23 | 24 | //In order to begin building localizable applications, set 25 | //CultureYouAreCodingWith in your .csproj file 26 | //inside a . For example, if you are using US english 27 | //in your source files, set the to en-US. Then uncomment 28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 29 | //the line below to match the UICulture setting in the project file. 30 | 31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 32 | 33 | 34 | [assembly: ThemeInfo( 35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 36 | //(used if a resource is not found in the page, 37 | // or application resource dictionaries) 38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 39 | //(used if a resource is not found in the page, 40 | // app, or any theme specific resource dictionaries) 41 | )] 42 | 43 | 44 | // Version information for an assembly consists of the following four values: 45 | // 46 | // Major Version 47 | // Minor Version 48 | // Build Number 49 | // Revision 50 | // 51 | // You can specify all the values or you can default the Build and Revision Numbers 52 | // by using the '*' as shown below: 53 | // [assembly: AssemblyVersion("1.0.*")] 54 | [assembly: AssemblyVersion("2.1.1.0")] 55 | [assembly: AssemblyFileVersion("2.1.1.0")] 56 | -------------------------------------------------------------------------------- /SunSync/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.18444 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace SunSync.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SunSync.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SunSync/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 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 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | -------------------------------------------------------------------------------- /SunSync/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.18444 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace SunSync.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SunSync/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /SunSync/Properties/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 52 | -------------------------------------------------------------------------------- /SunSync/QuickStartPage.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 41 | 48 | 55 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /SunSync/QuickStartPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using SunSync.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Text; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Input; 10 | using System.Windows.Media.Imaging; 11 | 12 | namespace SunSync 13 | { 14 | /// 15 | /// Interaction logic for QuickStartPage.xaml 16 | /// 17 | public partial class QuickStartPage : Page 18 | { 19 | private MainWindow mainWindow; 20 | private Dictionary syncRecordDict; 21 | private string jobsDbPath; 22 | private List topBGImages; 23 | private int clickCount; 24 | private string myAppPath; 25 | 26 | public QuickStartPage(MainWindow mainWindow) 27 | { 28 | InitializeComponent(); 29 | this.mainWindow = mainWindow; 30 | this.syncRecordDict = new Dictionary(); 31 | string myDocPath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); 32 | this.myAppPath = System.IO.Path.Combine(myDocPath, "qsunsync"); 33 | if (!Directory.Exists(myAppPath)) 34 | { 35 | try 36 | { 37 | Directory.CreateDirectory(myAppPath); 38 | } 39 | catch (Exception ex) 40 | { 41 | Log.Fatal(string.Format("unable to create my app path {0} due to {1}", myAppPath, ex.Message)); 42 | } 43 | } 44 | this.jobsDbPath = System.IO.Path.Combine(myDocPath, "qsunsync", "jobs.db"); 45 | this.topBGImages = new List(); 46 | this.topBGImages.Add("Pictures/qiniu_logo.jpg"); 47 | this.topBGImages.Add("Pictures/qiniu_logo.jpg"); 48 | this.clickCount = 0; 49 | } 50 | 51 | /// 52 | /// quick start page loaded event handler 53 | /// 54 | /// 55 | /// 56 | private void QuickStartPageLoaded_EventHandler(object sender, RoutedEventArgs e) 57 | { 58 | if (!File.Exists(this.jobsDbPath)) 59 | { 60 | try 61 | { 62 | SyncRecord.CreateSyncRecordDB(this.jobsDbPath); 63 | } 64 | catch (Exception ex) 65 | { 66 | Log.Fatal("create sync db failed, " + ex.Message); 67 | } 68 | } 69 | else 70 | { 71 | this.loadSyncRecords(); 72 | } 73 | 74 | this.checkAccountSetting(); 75 | } 76 | 77 | /// 78 | /// go to account setting page 79 | /// 80 | /// 81 | /// 82 | private void SetAccount_EventHandler(object sender, MouseButtonEventArgs e) 83 | { 84 | this.mainWindow.GotoAccountPage(); 85 | } 86 | 87 | 88 | private void SetDomains_EventHandler(object sender, MouseButtonEventArgs e) 89 | { 90 | this.mainWindow.GotoDomainsPage(); 91 | } 92 | 93 | /// 94 | /// go to empty sync setting page, create new sync job 95 | /// 96 | /// 97 | /// 98 | private void CreateNewSyncJob_EventHandler(object sender, MouseButtonEventArgs e) 99 | { 100 | this.mainWindow.GotoSyncSettingPage(null); 101 | } 102 | 103 | /// 104 | /// load recent sync jobs 105 | /// 106 | internal void loadSyncRecords() 107 | { 108 | try 109 | { 110 | List syncRecords = SyncRecord.LoadRecentSyncJobs(this.jobsDbPath); 111 | this.SyncHistoryListBox.Items.Clear(); 112 | this.syncRecordDict.Clear(); 113 | int index = 0; 114 | foreach (SyncRecord record in syncRecords) 115 | { 116 | ListBoxItem listBoxItem = new ListBoxItem(); 117 | Style ctlStyle = Application.Current.TryFindResource("jobListItemResource") as Style; 118 | listBoxItem.DataContext = record; 119 | listBoxItem.Style = ctlStyle; 120 | listBoxItem.MouseDoubleClick += listBoxItem_MouseDoubleClick; 121 | listBoxItem.MouseRightButtonUp += listBoxItem_MouseRightButtonUp; 122 | 123 | this.syncRecordDict.Add(listBoxItem, record.SyncId); 124 | this.SyncHistoryListBox.Items.Add(listBoxItem); 125 | index += 1; 126 | } 127 | } 128 | catch (Exception ex) 129 | { 130 | Log.Error("load recent sync jobs failed, " + ex.Message); 131 | } 132 | } 133 | 134 | private void listBoxItem_MouseRightButtonUp(object sender, MouseButtonEventArgs e) 135 | { 136 | ContextMenu ctxMenu = new ContextMenu(); 137 | 138 | MenuItem deleteJobMenuItem = new MenuItem(); 139 | deleteJobMenuItem.Header = "删除任务"; 140 | deleteJobMenuItem.Click += deleteJobMenuItem_Click; 141 | 142 | MenuItem exportJobLogMenuItem = new MenuItem(); 143 | exportJobLogMenuItem.Header = "导出日志"; 144 | exportJobLogMenuItem.Click += exportJobLogMenuItem_Click; 145 | 146 | ctxMenu.Items.Add(deleteJobMenuItem); 147 | ctxMenu.Items.Add(exportJobLogMenuItem); 148 | 149 | ListBoxItem selectedItem = (ListBoxItem)sender; 150 | selectedItem.ContextMenu = ctxMenu; 151 | } 152 | 153 | void exportJobLogMenuItem_Click(object sender, RoutedEventArgs e) 154 | { 155 | object selectedItem = this.SyncHistoryListBox.SelectedItem; 156 | if (selectedItem != null) 157 | { 158 | string jobId = this.syncRecordDict[(ListBoxItem)selectedItem]; 159 | SyncSetting syncSetting = SyncSetting.LoadSyncSettingByJobId(jobId); 160 | if (syncSetting != null) 161 | { 162 | System.Windows.Forms.SaveFileDialog dlg = new System.Windows.Forms.SaveFileDialog(); 163 | dlg.Title = "选择保存文件"; 164 | dlg.Filter = "Log (*.log)|*.log"; 165 | 166 | System.Windows.Forms.DialogResult dr = dlg.ShowDialog(); 167 | if (dr.Equals(System.Windows.Forms.DialogResult.OK)) 168 | { 169 | string logSaveFilePath = dlg.FileName; 170 | LogExporter.exportLog( 171 | Path.Combine(this.myAppPath, "logs", jobId, "success.log"), 172 | Path.Combine(this.myAppPath, "logs", jobId, "error.log"), 173 | Path.Combine(this.myAppPath, "logs", jobId, "skipped.log"), 174 | Path.Combine(this.myAppPath, "logs", jobId, "exists.log"), 175 | Path.Combine(this.myAppPath, "logs", jobId, "not_overwrite.log"), 176 | Path.Combine(this.myAppPath, "logs", jobId, "overwrite.log"), 177 | logSaveFilePath); 178 | } 179 | } 180 | } 181 | } 182 | 183 | void deleteJobMenuItem_Click(object sender, RoutedEventArgs e) 184 | { 185 | object selectedItem = this.SyncHistoryListBox.SelectedItem; 186 | if (selectedItem != null) 187 | { 188 | string jobId = this.syncRecordDict[(ListBoxItem)selectedItem]; 189 | SyncSetting syncSetting = SyncSetting.LoadSyncSettingByJobId(jobId); 190 | if (syncSetting != null) 191 | { 192 | MessageBoxResult mbr = MessageBox.Show( 193 | string.Format("确认删除同步任务 {0} -> {1} 么?", syncSetting.SyncLocalDir, syncSetting.SyncTargetBucket), "删除任务", 194 | MessageBoxButton.YesNo, MessageBoxImage.Question); 195 | if (mbr.Equals(MessageBoxResult.Yes)) 196 | { 197 | //delete job related files 198 | string[] filesToDelete ={ 199 | Path.Combine(this.myAppPath,"logs",jobId,"error.log"), 200 | Path.Combine(this.myAppPath,"logs",jobId,"exists.log"), 201 | Path.Combine(this.myAppPath,"logs",jobId,"not_overwrite.log"), 202 | Path.Combine(this.myAppPath,"logs",jobId,"overwrite.log"), 203 | Path.Combine(this.myAppPath,"logs",jobId,"skipped.log"), 204 | Path.Combine(this.myAppPath,"logs",jobId,"success.log"), 205 | Path.Combine(this.myAppPath,"synclog",jobId+".log.db"), 206 | Path.Combine(this.myAppPath,"dircache",jobId+".done") 207 | }; 208 | 209 | foreach (string path in filesToDelete) 210 | { 211 | try 212 | { 213 | File.Delete(path); 214 | } 215 | catch (Exception ex) 216 | { 217 | Log.Error(string.Format("delete file {0} failed due to {1}", path, ex.Message)); 218 | } 219 | } 220 | 221 | string[] foldersToDelete ={ 222 | Path.Combine(this.myAppPath,"logs",jobId) 223 | }; 224 | 225 | foreach (string path in foldersToDelete) 226 | { 227 | try 228 | { 229 | Directory.Delete(path); 230 | } 231 | catch (Exception ex) 232 | { 233 | Log.Error(string.Format("delete folder {0} failed due to {1}", path, ex.Message)); 234 | } 235 | } 236 | 237 | try 238 | { 239 | SyncRecord.DeleteSyncJobById(jobId, this.jobsDbPath); 240 | } 241 | catch (Exception ex) 242 | { 243 | Log.Error("delete sync job by id error, " + ex.Message); 244 | } 245 | 246 | this.SyncHistoryListBox.Items.Remove(selectedItem); 247 | this.syncRecordDict.Remove((ListBoxItem)selectedItem); 248 | } 249 | } 250 | else 251 | { 252 | Log.Error("load sync setting by id failed, " + jobId); 253 | } 254 | } 255 | } 256 | 257 | 258 | private void listBoxItem_MouseDoubleClick(object sender, MouseButtonEventArgs e) 259 | { 260 | object selectedItem = this.SyncHistoryListBox.SelectedItem; 261 | if (selectedItem != null) 262 | { 263 | string jobId = this.syncRecordDict[(ListBoxItem)selectedItem]; 264 | SyncSetting syncSetting = SyncSetting.LoadSyncSettingByJobId(jobId); 265 | if (syncSetting != null) 266 | { 267 | this.mainWindow.GotoSyncSettingPage(syncSetting); 268 | } 269 | else 270 | { 271 | Log.Error("load sync setting by id failed, " + jobId); 272 | } 273 | } 274 | } 275 | 276 | /// 277 | /// check ak & sk settings 278 | /// 279 | internal void checkAccountSetting() 280 | { 281 | Account account = Account.TryLoadAccount(); 282 | if (string.IsNullOrEmpty(account.AccessKey) || string.IsNullOrEmpty(account.SecretKey)) 283 | { 284 | Log.Info("no account info found"); 285 | this.CreateNewTask_TextBlock.Foreground = System.Windows.Media.Brushes.Gray; 286 | this.CreateNewTask_TextBlock.IsEnabled = false; 287 | } 288 | else 289 | { 290 | this.CreateNewTask_TextBlock.Foreground = System.Windows.Media.Brushes.MediumBlue; 291 | this.CreateNewTask_TextBlock.IsEnabled = true; 292 | } 293 | } 294 | 295 | private void ChangeTopBgImage_EventHandler(object sender, MouseButtonEventArgs e) 296 | { 297 | int imgCnt = this.topBGImages.Count; 298 | clickCount += 1; 299 | int index = clickCount % imgCnt; 300 | this.TopLogoImage.Source = new BitmapImage(new Uri(this.topBGImages[index], UriKind.Relative)); 301 | } 302 | 303 | private void AboutApp_EventHandler(object sender, MouseButtonEventArgs e) 304 | { 305 | try 306 | { 307 | Process.Start("https://github.com/qiniu/qsunsync"); 308 | } 309 | catch (Exception) { } 310 | } 311 | 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /SunSync/SunSync.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {1D91C666-3321-4ED9-9777-889CC4BAB795} 8 | WinExe 9 | Properties 10 | SunSync 11 | QSunSync 12 | v4.0 13 | 512 14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 4 16 | 17 | false 18 | 19 | 20 | F:\publish\ 21 | true 22 | Disk 23 | false 24 | Foreground 25 | 7 26 | Days 27 | false 28 | false 29 | true 30 | 0 31 | 1.4.1.%2a 32 | false 33 | true 34 | true 35 | 36 | 37 | AnyCPU 38 | true 39 | full 40 | false 41 | bin\Debug\ 42 | DEBUG;TRACE 43 | prompt 44 | 4 45 | 46 | 47 | AnyCPU 48 | pdbonly 49 | true 50 | bin\Release\ 51 | TRACE 52 | prompt 53 | 4 54 | 55 | 56 | true 57 | bin\x86\Debug\ 58 | DEBUG;TRACE 59 | full 60 | x86 61 | prompt 62 | MinimumRecommendedRules.ruleset 63 | 64 | 65 | bin\x86\Release\ 66 | TRACE 67 | true 68 | pdbonly 69 | x86 70 | prompt 71 | MinimumRecommendedRules.ruleset 72 | 73 | 74 | SunSync.App 75 | 76 | 77 | sunsync.ico 78 | 79 | 80 | false 81 | 82 | 83 | sunsync.pfx 84 | 85 | 86 | 87 | 333DFD5467FF6A719FA301181AEE45C9DC69FAD8 88 | 89 | 90 | codesign.pfx 91 | 92 | 93 | true 94 | 95 | 96 | LocalIntranet 97 | 98 | 99 | 100 | false 101 | 102 | 103 | Properties\app.manifest 104 | 105 | 106 | true 107 | bin\x64\Debug\ 108 | DEBUG;TRACE 109 | full 110 | x64 111 | prompt 112 | MinimumRecommendedRules.ruleset 113 | 114 | 115 | bin\x64\Release\ 116 | TRACE 117 | true 118 | pdbonly 119 | x64 120 | prompt 121 | MinimumRecommendedRules.ruleset 122 | 123 | 124 | 125 | ..\packages\Newtonsoft.Json.10.0.3\lib\net40\Newtonsoft.Json.dll 126 | 127 | 128 | False 129 | ..\..\csharp-sdk\src\Qiniu\bin\x64\Debug\Qiniu.dll 130 | 131 | 132 | 133 | 134 | ..\packages\System.Data.SQLite.Core.1.0.105.2\lib\net40\System.Data.SQLite.dll 135 | True 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 4.0 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | MSBuild:Compile 154 | Designer 155 | 156 | 157 | DomainsSettingPage.xaml 158 | 159 | 160 | AccountSettingPage.xaml 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | True 177 | True 178 | Resources.resx 179 | 180 | 181 | QuickStartPage.xaml 182 | 183 | 184 | SyncProgressPage.xaml 185 | 186 | 187 | SyncResultPage.xaml 188 | 189 | 190 | SyncSettingPage.xaml 191 | 192 | 193 | MSBuild:Compile 194 | Designer 195 | 196 | 197 | Designer 198 | MSBuild:Compile 199 | 200 | 201 | MSBuild:Compile 202 | Designer 203 | 204 | 205 | App.xaml 206 | Code 207 | 208 | 209 | MainWindow.xaml 210 | Code 211 | 212 | 213 | Designer 214 | MSBuild:Compile 215 | 216 | 217 | Designer 218 | MSBuild:Compile 219 | 220 | 221 | Designer 222 | MSBuild:Compile 223 | 224 | 225 | Designer 226 | MSBuild:Compile 227 | 228 | 229 | 230 | 231 | Code 232 | 233 | 234 | True 235 | Settings.settings 236 | True 237 | 238 | 239 | ResXFileCodeGenerator 240 | Resources.Designer.cs 241 | Designer 242 | 243 | 244 | 245 | 246 | 247 | SettingsSingleFileGenerator 248 | Settings.Designer.cs 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | Always 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | False 280 | Microsoft .NET Framework 4 %28x86 和 x64%29 281 | true 282 | 283 | 284 | False 285 | .NET Framework 3.5 SP1 Client Profile 286 | false 287 | 288 | 289 | False 290 | .NET Framework 3.5 SP1 291 | false 292 | 293 | 294 | False 295 | Windows Installer 4.5 296 | true 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。 307 | 308 | 309 | 310 | 317 | -------------------------------------------------------------------------------- /SunSync/SunSync.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | F:\publish\|publish\ 5 | 6 | 7 | 8 | 9 | 10 | en-US 11 | false 12 | 13 | 14 | false 15 | 16 | -------------------------------------------------------------------------------- /SunSync/SunSync.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SunSync", "SunSync\SunSync.csproj", "{1D91C666-3321-4ED9-9777-889CC4BAB795}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Debug|x86 = Debug|x86 10 | Release|Any CPU = Release|Any CPU 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|x86.ActiveCfg = Debug|x86 17 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Debug|x86.Build.0 = Debug|x86 18 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|x86.ActiveCfg = Release|x86 21 | {1D91C666-3321-4ED9-9777-889CC4BAB795}.Release|x86.Build.0 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /SunSync/SunSync.v11.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/SunSync.v11.suo -------------------------------------------------------------------------------- /SunSync/SyncProgressPage.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /SunSync/SyncResultPage.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 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 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /SunSync/SyncResultPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using SunSync.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | 9 | namespace SunSync 10 | { 11 | /// 12 | /// Interaction logic for SyncResultPage.xaml 13 | /// 14 | public partial class SyncResultPage : Page 15 | { 16 | private string jobId; 17 | private bool fileOverwrite; 18 | 19 | private int fileSkippedCount; 20 | private int fileExistsCount; 21 | private int fileOverwriteCount; 22 | private int fileNotOverwriteCount; 23 | private int fileUploadErrorCount; 24 | private int fileUploadSuccessCount; 25 | 26 | private string fileSkippedLogPath; 27 | private string fileExistsLogPath; 28 | private string fileOverwriteLogPath; 29 | private string fileNotOverwriteLogPath; 30 | private string fileUploadSuccessLogPath; 31 | private string fileUploadErrorLogPath; 32 | 33 | private Dictionary syncResultInfo; 34 | private TimeSpan spentTime; 35 | private MainWindow mainWindow; 36 | public SyncResultPage(MainWindow mainWindow) 37 | { 38 | InitializeComponent(); 39 | this.mainWindow = mainWindow; 40 | this.fileOverwrite = false; 41 | this.syncResultInfo = new Dictionary(); 42 | this.syncResultInfo.Add("UPLOAD_SUCCESS", "本次同步成功同步到七牛云空间中的文件数量。"); 43 | this.syncResultInfo.Add("UPLOAD_FAILURE", "本次同步因为各种原因没有成功同步到七牛云空间中的文件数量。"); 44 | this.syncResultInfo.Add("UPLOAD_SKIPPED", "本次同步按照指定的前缀或后缀忽略规则跳过不同步的文件数量。"); 45 | this.syncResultInfo.Add("UPLOAD_EXISTS_MATCH", "本次同步过程中发现的已存在于云空间且本地未改动的文件数量,这些文件本地和空间内容一致,所以同步过程中自动跳过。"); 46 | this.syncResultInfo.Add("UPLOAD_EXISTS_NO_OVERWRITE", "本次同步过程中发现的已存在于云空间且本地已有改动的文件数量,这些文件没有进行覆盖上传。如果需要覆盖上传,请在同步设置里面勾选覆盖选项。"); 47 | this.syncResultInfo.Add("UPLOAD_EXISTS_OVERWRITE", "本次同步过程中发现的已存在于云空间且本地已有改动的文件数量,这些文件进行了覆盖上传。"); 48 | } 49 | 50 | public void LoadSyncResult(string jobId, TimeSpan spentTime, bool fileOverwrite, 51 | int fileSkippedCount, string fileSkippedLogPath, 52 | int fileExistsCount, string fileExistsLogPath, 53 | int fileOverwriteCount, string fileOverwriteLogPath, 54 | int fileNotOverwriteCount, string fileNotOverwriteLogPath, 55 | int fileUploadErrorCount, string fileUploadErrorLogPath, 56 | int fileUploadSuccessCount, string fileUploadSuccessLogPath) 57 | { 58 | this.jobId = jobId; 59 | this.spentTime = spentTime; 60 | 61 | this.fileSkippedCount = fileSkippedCount; 62 | this.fileOverwrite = fileOverwrite; 63 | this.fileExistsCount = fileExistsCount; 64 | this.fileOverwriteCount = fileOverwriteCount; 65 | this.fileNotOverwriteCount = fileNotOverwriteCount; 66 | this.fileUploadErrorCount = fileUploadErrorCount; 67 | this.fileUploadSuccessCount = fileUploadSuccessCount; 68 | 69 | this.fileSkippedLogPath = fileSkippedLogPath; 70 | this.fileOverwriteLogPath = fileOverwriteLogPath; 71 | this.fileExistsLogPath = fileExistsLogPath; 72 | this.fileNotOverwriteLogPath = fileNotOverwriteLogPath; 73 | this.fileUploadErrorLogPath = fileUploadErrorLogPath; 74 | this.fileUploadSuccessLogPath = fileUploadSuccessLogPath; 75 | } 76 | 77 | private void SyncResultLoaded_EventHandler(object sender, RoutedEventArgs e) 78 | { 79 | Log.Info(string.Format("sync last total time {0}", this.spentTimeStr(this.spentTime.TotalSeconds))); 80 | //set title 81 | this.SyncResultTitleTextBlock.Text = this.spentTimeStr(this.spentTime.TotalSeconds); 82 | 83 | this.UploadSuccessTextBlock1.Text = string.Format("同步成功: {0}", this.fileUploadSuccessCount); 84 | this.UploadSuccessTextBlock2.Text = syncResultInfo["UPLOAD_SUCCESS"]; 85 | 86 | this.UploadFailureTextBlock1.Text = string.Format("同步失败: {0}", this.fileUploadErrorCount); 87 | this.UploadFailureTextBlock2.Text = syncResultInfo["UPLOAD_FAILURE"]; 88 | 89 | this.UploadSkippedTextBlock1.Text = string.Format("规则跳过: {0}", this.fileSkippedCount); 90 | this.UploadSkippedTextBlock2.Text = syncResultInfo["UPLOAD_SKIPPED"]; 91 | 92 | this.UploadExistsTextBlock1.Text = string.Format("智能跳过: {0}", this.fileExistsCount); 93 | this.UploadExistsTextBlock2.Text = syncResultInfo["UPLOAD_EXISTS_MATCH"]; 94 | 95 | if (this.fileOverwrite) 96 | { 97 | this.UploadOverwriteTextBlock1.Text = string.Format("强制覆盖: {0}", this.fileOverwriteCount); 98 | this.UploadOverwriteTextBlock2.Text = syncResultInfo["UPLOAD_EXISTS_OVERWRITE"]; 99 | } 100 | else 101 | { 102 | this.UploadOverwriteTextBlock1.Text = string.Format("未覆盖: {0}", this.fileNotOverwriteCount); 103 | this.UploadOverwriteTextBlock2.Text = syncResultInfo["UPLOAD_EXISTS_NO_OVERWRITE"]; 104 | } 105 | } 106 | 107 | private string spentTimeStr(double seconds) 108 | { 109 | string result = ""; 110 | if (seconds < 60) 111 | { 112 | result = string.Format("同步结果 - 耗时 {0} 秒", seconds.ToString("F")); 113 | } 114 | else if (seconds < 60 * 60) 115 | { 116 | result = string.Format("同步结果 - 耗时 {0} 分", (seconds / 60).ToString("F")); 117 | } 118 | else 119 | { 120 | result = string.Format("同步结果 - 耗时 {0} 时", (seconds / 60 / 60).ToString("F")); 121 | } 122 | 123 | return result; 124 | } 125 | 126 | private void ExportLog_EventHandler(object sender, RoutedEventArgs e) 127 | { 128 | System.Windows.Forms.SaveFileDialog dlg = new System.Windows.Forms.SaveFileDialog(); 129 | dlg.Title = "选择保存文件"; 130 | dlg.Filter = "Log (*.log)|*.log"; 131 | 132 | System.Windows.Forms.DialogResult dr = dlg.ShowDialog(); 133 | if (dr.Equals(System.Windows.Forms.DialogResult.OK)) 134 | { 135 | string logFilePath = dlg.FileName; 136 | LogExporter.exportLog(this.fileUploadSuccessLogPath, 137 | this.fileUploadErrorLogPath, 138 | this.fileSkippedLogPath, 139 | this.fileExistsLogPath, 140 | this.fileNotOverwriteLogPath, 141 | this.fileOverwriteLogPath,logFilePath); 142 | } 143 | } 144 | 145 | private void BackToHome_EventHandler(object sender, RoutedEventArgs e) 146 | { 147 | this.mainWindow.GotoHomePage(); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /SunSync/SyncSettingPage.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 91 | 92 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 116 | 117 | 118 | 119 | 120 | 124 | 125 | 126 | 127 | 128 | 132 | 133 | 134 | 135 | 136 | 138 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /SunSync/SyncSettingPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Input; 6 | using System.Windows.Forms; 7 | using SunSync.Models; 8 | using Qiniu.Util; 9 | using System.Threading; 10 | using System; 11 | using Qiniu.Storage; 12 | namespace SunSync 13 | { 14 | /// 15 | /// Interaction logic for SyncSettingPage.xaml 16 | /// 17 | public partial class SyncSettingPage : Page 18 | { 19 | private bool uiInited; 20 | //default chunk size 21 | //from version 2.1.0, this variable stands for block upload thread count 22 | private int defaultChunkSize; 23 | //upload entry domain 24 | private int uploadEntryDomain; 25 | private MainWindow mainWindow; 26 | 27 | private Account account; 28 | private Domains domains; 29 | private SyncSetting syncSetting; 30 | private BucketManager bucketManager; 31 | 32 | public SyncSettingPage(MainWindow mainWindow) 33 | { 34 | InitializeComponent(); 35 | this.mainWindow = mainWindow; 36 | } 37 | 38 | /// 39 | /// load sync settings, this method is called before the page loaded method 40 | /// 41 | /// 42 | public void LoadSyncSetting(SyncSetting syncSetting) 43 | { 44 | this.syncSetting = syncSetting; 45 | this.bucketManager = null; 46 | } 47 | 48 | /// 49 | /// sync setting page loaded event handler 50 | /// 51 | /// 52 | /// 53 | private void SyncSettingPageLoaded_EventHandler(object sender, RoutedEventArgs e) 54 | { 55 | //set global domains 56 | this.domains = Domains.TryLoadDomains(); 57 | if (this.domains != null && !string.IsNullOrEmpty(this.domains.RsDomain) && 58 | !string.IsNullOrEmpty(this.domains.UpDomain)) 59 | { 60 | SystemConfig.RS_DOMAIN = this.domains.RsDomain; 61 | SystemConfig.UP_DOMAIN = this.domains.UpDomain; 62 | //set qiniu global rs host 63 | Qiniu.Storage.Config.DefaultRsHost = this.domains.RsDomain; 64 | } 65 | else 66 | { 67 | SystemConfig.RS_DOMAIN = ""; 68 | SystemConfig.UP_DOMAIN = ""; 69 | Qiniu.Storage.Config.DefaultRsHost = "rs.qiniu.com"; 70 | } 71 | //init bucket manager 72 | this.initBucketManager(); 73 | if (this.bucketManager != null) 74 | { 75 | //clear old buckets 76 | this.SyncTargetBucketsComboBox.ItemsSource = null; 77 | new Thread(new ThreadStart(this.reloadBuckets)).Start(); 78 | Thread.Sleep(10); 79 | } 80 | this.initUIDefaults(); 81 | this.uiInited = true; 82 | } 83 | 84 | /// 85 | /// init the bucket manager 86 | /// 87 | private void initBucketManager() 88 | { 89 | this.account = Account.TryLoadAccount(); 90 | 91 | if (string.IsNullOrEmpty(account.AccessKey) || string.IsNullOrEmpty(account.SecretKey)) 92 | { 93 | Log.Info("account info not set"); 94 | this.SettingsErrorTextBlock.Text = "请返回设置 AK 和 SK"; 95 | return; 96 | } 97 | Mac mac = new Mac(this.account.AccessKey, this.account.SecretKey); 98 | Config config =new Config(); 99 | if (this.domains != null && !string.IsNullOrEmpty(domains.RsDomain)) 100 | { 101 | Qiniu.Storage.Config.DefaultRsHost = domains.RsDomain; 102 | config.Zone = new Zone 103 | { 104 | RsHost = domains.RsDomain, 105 | }; 106 | } 107 | else 108 | { 109 | Qiniu.Storage.Config.DefaultRsHost = "rs.qiniu.com"; 110 | config.Zone = Zone.ZONE_CN_East; 111 | } 112 | this.bucketManager = new BucketManager(mac, config); 113 | } 114 | 115 | /// 116 | /// init the ui values according to the syncSetting parameter 117 | /// 118 | private void initUIDefaults() 119 | { 120 | //ui settings 121 | this.SyncSettingTabControl.SelectedIndex = 0; 122 | this.SettingsErrorTextBlock.Text = ""; 123 | if (this.syncSetting == null) 124 | { 125 | //basic settings 126 | this.SyncLocalFolderTextBox.Text = ""; 127 | this.SyncTargetBucketsComboBox.SelectedIndex = -1; 128 | this.FileTypeComboBox.SelectedIndex = 0; 129 | this.CheckRemoteDuplicateCheckBox.IsChecked = false; 130 | //advanced settings 131 | this.PrefixTextBox.Text = ""; 132 | this.CheckNewFilesCheckBox.IsChecked = false; 133 | this.OverwriteFileCheckBox.IsChecked = false; 134 | this.IgnoreDirCheckBox.IsChecked = false; 135 | this.SkipPrefixesTextBox.Text = ""; 136 | this.SkipSuffixesTextBox.Text = ""; 137 | this.ChunkDefaultSizeSlider.Value = 1; //1 138 | this.ChunkDefaultSizeLabel.Content = "1"; 139 | this.ChunkUploadThresholdSlider.Value = 4;//4MB 140 | this.ThreadCountSlider.Value = 10; 141 | this.ThreadCountLabel.Content = "10"; 142 | this.UploadByCdnRadioButton.IsChecked = true; 143 | } 144 | else 145 | { 146 | //basic settings 147 | this.SyncLocalFolderTextBox.Text = syncSetting.SyncLocalDir; 148 | this.FileTypeComboBox.SelectedIndex = syncSetting.FileType; 149 | this.SyncTargetBucketsComboBox.SelectedIndex = -1; 150 | this.CheckRemoteDuplicateCheckBox.IsChecked = syncSetting.CheckRemoteDuplicate; 151 | //advanced settings 152 | this.PrefixTextBox.Text = syncSetting.SyncPrefix; 153 | this.OverwriteFileCheckBox.IsChecked = syncSetting.OverwriteFile; 154 | this.CheckNewFilesCheckBox.IsChecked = syncSetting.CheckNewFiles; 155 | this.IgnoreDirCheckBox.IsChecked = syncSetting.IgnoreDir; 156 | this.SkipPrefixesTextBox.Text = syncSetting.SkipPrefixes; 157 | this.SkipSuffixesTextBox.Text = syncSetting.SkipSuffixes; 158 | this.ThreadCountSlider.Value = syncSetting.SyncThreadCount; 159 | this.ThreadCountLabel.Content = syncSetting.SyncThreadCount.ToString(); 160 | this.ChunkUploadThresholdSlider.Value = syncSetting.ChunkUploadThreshold / 1024 / 1024; 161 | this.ChunkDefaultSizeSlider.Value = syncSetting.DefaultChunkSize; 162 | this.ChunkDefaultSizeLabel.Content = syncSetting.DefaultChunkSize.ToString(); 163 | switch (syncSetting.UploadEntryDomain) { 164 | case 0: 165 | this.UploadByCdnRadioButton.IsChecked = true; 166 | break; 167 | case 1: 168 | this.UploadBySrcRadioButton.IsChecked = true; 169 | break; 170 | default: 171 | this.UploadByCdnRadioButton.IsChecked = true; 172 | break; 173 | } 174 | } 175 | } 176 | 177 | //reload buckets 178 | private void reloadBuckets() 179 | { 180 | DateTime start = System.DateTime.Now; 181 | //get new bucket list 182 | BucketsResult bucketsResult = this.bucketManager.Buckets(true); 183 | if (bucketsResult.Code == 200) 184 | { 185 | List buckets = bucketsResult.Result; 186 | Dispatcher.Invoke(new Action(delegate 187 | { 188 | this.SyncTargetBucketsComboBox.ItemsSource = buckets; 189 | if (this.syncSetting != null) 190 | { 191 | this.SyncTargetBucketsComboBox.SelectedItem = this.syncSetting.SyncTargetBucket; 192 | } 193 | Log.Info("load buckets last for " + System.DateTime.Now.Subtract(start).TotalSeconds + " seconds"); 194 | })); 195 | } 196 | else if (bucketsResult.Code == 401) 197 | { 198 | Dispatcher.Invoke(new Action(delegate 199 | { 200 | this.SettingsErrorTextBlock.Text = "AK 或 SK 不正确"; 201 | })); 202 | } 203 | else 204 | { 205 | string xReqId = "N/A"; 206 | if (bucketsResult.RefInfo != null && bucketsResult.RefInfo.ContainsKey("X-Reqid")) 207 | { 208 | xReqId = bucketsResult.RefInfo["X-Reqid"]; 209 | } 210 | Log.Error(string.Format("get buckets unknown error, {0}:{1}:{2}:{3}", bucketsResult.Code, bucketsResult.Text, xReqId, 211 | bucketsResult.Text)); 212 | string message = null; 213 | if (!string.IsNullOrEmpty(bucketsResult.RefText)) 214 | { 215 | message = string.Format("获取空间列表失败 {0}", bucketsResult.RefText); 216 | } 217 | else 218 | { 219 | message = "获取空间列表失败,网络故障!"; 220 | } 221 | 222 | Dispatcher.Invoke(new Action(delegate 223 | { 224 | this.SettingsErrorTextBlock.Text = message; 225 | })); 226 | } 227 | } 228 | 229 | /// 230 | /// reload buckets 231 | /// 232 | /// 233 | /// 234 | private void ReloadBucketButton_EventHandler(object sender, RoutedEventArgs e) 235 | { 236 | this.SettingsErrorTextBlock.Text = ""; 237 | this.SyncTargetBucketsComboBox.ItemsSource = null; 238 | new Thread(new ThreadStart(this.reloadBuckets)).Start(); 239 | } 240 | 241 | /// 242 | /// back to home event handler 243 | /// 244 | /// 245 | /// 246 | private void BackToHome_EventHandler(object sender, MouseButtonEventArgs e) 247 | { 248 | this.mainWindow.GotoHomePage(); 249 | } 250 | 251 | /// 252 | /// browse the local folder to sync 253 | /// 254 | /// 255 | /// 256 | private void BrowseFolderButton_EventHandler(object sender, RoutedEventArgs e) 257 | { 258 | FolderBrowserDialog fbd = new FolderBrowserDialog(); 259 | fbd.ShowNewFolderButton = false; 260 | DialogResult dr = fbd.ShowDialog(); 261 | if (dr.Equals(DialogResult.OK)) 262 | { 263 | this.SyncLocalFolderTextBox.Text = fbd.SelectedPath; 264 | } 265 | } 266 | 267 | 268 | private void StartSyncButton_EventHandler(object sender, RoutedEventArgs e) 269 | { 270 | this.SyncSettingTabControl.SelectedIndex = 0; 271 | //check ak & sk 272 | if (string.IsNullOrEmpty(this.account.AccessKey) 273 | || string.IsNullOrEmpty(this.account.SecretKey)) 274 | { 275 | this.SettingsErrorTextBlock.Text = "请返回设置 AK & SK"; 276 | return; 277 | } 278 | 279 | //save config to job record 280 | if (this.SyncLocalFolderTextBox.Text.Trim().Length == 0) 281 | { 282 | this.SettingsErrorTextBlock.Text = "请选择本地待同步目录"; 283 | return; 284 | } 285 | 286 | if (this.SyncTargetBucketsComboBox.SelectedIndex == -1) 287 | { 288 | this.SettingsErrorTextBlock.Text = "请选择同步的目标空间"; 289 | return; 290 | } 291 | 292 | string syncLocalDir = this.SyncLocalFolderTextBox.Text.Trim(); 293 | if (!Directory.Exists(syncLocalDir)) 294 | { 295 | //directory not found 296 | this.SyncSettingTabControl.SelectedIndex = 0; 297 | this.SettingsErrorTextBlock.Text = "本地待同步目录不存在"; 298 | return; 299 | } 300 | 301 | string syncTargetBucket = this.SyncTargetBucketsComboBox.SelectedItem.ToString(); 302 | StatResult statResult = this.bucketManager.Stat(syncTargetBucket, "NONE_EXIST_KEY"); 303 | 304 | if (statResult.Code == 401) 305 | { 306 | //ak & sk not right 307 | this.SettingsErrorTextBlock.Text = "AK 或 SK 不正确"; 308 | return; 309 | } 310 | else if (statResult.Code == 631) 311 | { 312 | //bucket not exist 313 | this.SettingsErrorTextBlock.Text = "指定空间不存在"; 314 | return; 315 | } 316 | else if (statResult.Code == 612 317 | || statResult.Code == 200) 318 | { 319 | //file exists or not 320 | //ignore 321 | } 322 | else 323 | { 324 | this.SettingsErrorTextBlock.Text = "网络故障"; 325 | Log.Error(string.Format("get buckets unknown error, {0}:{1}:{2}:{3}", statResult.Code, 326 | statResult.Text, statResult.RefInfo["X-Reqid"],System.Text.Encoding.UTF8.GetString(statResult.Data))); 327 | return; 328 | } 329 | 330 | //set progress ak & sk 331 | SystemConfig.ACCESS_KEY = this.account.AccessKey; 332 | SystemConfig.SECRET_KEY = this.account.SecretKey; 333 | 334 | //optional settings 335 | SyncSetting syncSetting = new SyncSetting(); 336 | syncSetting.SyncLocalDir = syncLocalDir; 337 | syncSetting.SyncTargetBucket = syncTargetBucket; 338 | syncSetting.CheckRemoteDuplicate = this.CheckRemoteDuplicateCheckBox.IsChecked.Value; 339 | syncSetting.SyncPrefix = this.PrefixTextBox.Text.Trim(); 340 | syncSetting.CheckNewFiles = this.CheckNewFilesCheckBox.IsChecked.Value; 341 | syncSetting.IgnoreDir = this.IgnoreDirCheckBox.IsChecked.Value; 342 | syncSetting.SkipPrefixes = this.SkipPrefixesTextBox.Text.Trim(); 343 | syncSetting.SkipSuffixes = this.SkipSuffixesTextBox.Text.Trim(); 344 | syncSetting.OverwriteFile = this.OverwriteFileCheckBox.IsChecked.Value; 345 | syncSetting.SyncThreadCount = (int)this.ThreadCountSlider.Value; 346 | syncSetting.ChunkUploadThreshold = (int)this.ChunkUploadThresholdSlider.Value * 1024 * 1024; 347 | syncSetting.DefaultChunkSize = this.defaultChunkSize; 348 | syncSetting.UploadEntryDomain = this.uploadEntryDomain; 349 | syncSetting.FileType = this.FileTypeComboBox.SelectedIndex; 350 | 351 | this.mainWindow.GotoSyncProgress(syncSetting); 352 | } 353 | 354 | private void ChunkUploadThresholdChange_EventHandler(object sender, RoutedPropertyChangedEventArgs e) 355 | { 356 | if (this.ChunkUploadThresholdLabel != null) 357 | { 358 | this.ChunkUploadThresholdLabel.Content = this.ChunkUploadThresholdSlider.Value + "MB"; 359 | } 360 | } 361 | 362 | private void ThreadCountChange_EventHandler(object sender, RoutedPropertyChangedEventArgs e) 363 | { 364 | if (this.ThreadCountLabel != null) 365 | { 366 | this.ThreadCountLabel.Content = this.ThreadCountSlider.Value + ""; 367 | } 368 | } 369 | 370 | private void UploadByCdnRadioButton_Checked(object sender, RoutedEventArgs e) 371 | { 372 | this.uploadEntryDomain = 0; 373 | } 374 | 375 | private void UploadBySrcRadioButton_Checked(object sender, RoutedEventArgs e) 376 | { 377 | this.uploadEntryDomain = 1; 378 | } 379 | 380 | private void CheckRemoteDuplicateCheckBox_Checked(object sender, RoutedEventArgs e) 381 | { 382 | if (this.uiInited) 383 | { 384 | System.Windows.Forms.MessageBox.Show("您选中这个选项是因为空间可能存在同名文件吗?如果您想覆盖这些同名文件,请在【高级设置】里面选中【覆盖空间中已有同名文件】的选项,否则默认情况下,不会帮您覆盖的!", 385 | "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); 386 | } 387 | } 388 | 389 | private void ChunkDefaultSizeChange_EventHandler(object sender, RoutedPropertyChangedEventArgs e) 390 | { 391 | 392 | this.defaultChunkSize = (int)this.ChunkDefaultSizeSlider.Value; 393 | if (this.uiInited) 394 | { 395 | this.ChunkDefaultSizeLabel.Content = this.ChunkDefaultSizeSlider.Value.ToString(); 396 | } 397 | } 398 | } 399 | 400 | } 401 | -------------------------------------------------------------------------------- /SunSync/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SunSync/images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/back.png -------------------------------------------------------------------------------- /SunSync/images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/cloud.png -------------------------------------------------------------------------------- /SunSync/images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/folder.png -------------------------------------------------------------------------------- /SunSync/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/home.png -------------------------------------------------------------------------------- /SunSync/images/qiniu_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/qiniu_logo.jpg -------------------------------------------------------------------------------- /SunSync/images/reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/reload.png -------------------------------------------------------------------------------- /SunSync/images/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/result.png -------------------------------------------------------------------------------- /SunSync/images/sun_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/sun_logo.jpg -------------------------------------------------------------------------------- /SunSync/images/sunsync.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/images/sunsync.ico -------------------------------------------------------------------------------- /SunSync/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SunSync/sunsync.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/sunsync.ico -------------------------------------------------------------------------------- /SunSync/sunsync.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/SunSync/sunsync.pfx -------------------------------------------------------------------------------- /docs/QSunSync-v1.7.0-使用手册.html: -------------------------------------------------------------------------------- 1 | QSunSync 七牛云文件同步工具使用手册
5 | 6 | 7 | 8 | 9 | 10 |

QSunSync 七牛云文件同步工具使用手册

11 | 12 |
70 |
71 | 72 | 73 | 74 |

1.初始界面

75 | 76 |

打开同步工具之后,出现“初始界面”(界面 1)。

77 | 78 | 79 | 80 |

1-01

81 | 82 |

界面1 初始界面

83 | 84 |

初始状态下,由于尚未设置账号,因此“新建同步任务”处于不可用状态,关于“账号设置”请参考后续介绍。

85 | 86 | 87 | 88 |

1.1 最近同步任务

89 | 90 |

保留最近同步的项目(参考后文介绍)。显示项目的同步时间、目标空间及本地同步目录。通过双击某条项目纪录,可以直接跳转至同步设置界面(界面 3),且所有配置与之前同步该项目时配置保持一致。若两次将同一个目录上传到同一个空间,只会保留最近的一条记录。

91 | 92 | 93 | 94 |

1.2 快速开始

95 | 96 | 97 | 98 |
1.2.1 新建同步任务
99 | 100 |

点击此处将跳转至“同步设置界面”(界面 3)新建同步任务。若之前从未在“账号 设置界面”中配置过 AK/SK 信息,该选项将为灰色,点击不会跳转。

101 | 102 | 103 | 104 |
1.2.2 账号设置
105 | 106 |

点击此处将跳转至“账号设置界面”(界面 2),输入 AK/SK 登陆 QSunSync。一旦登录成功将记录 AK/SK,之后再次使用时,不需重新配置。

107 | 108 | 109 | 110 |
1.2.3 关于
111 | 112 |

点击此处跳转到网页介绍。

113 | 114 | 115 | 116 |

1.3 关闭程序

117 | 118 |

点击界面上的“关闭”按钮,程序会隐藏至托盘,但不终止运行。如需彻底关闭,右键点击托盘图标,点击“退出”即可。

119 | 120 | 121 | 122 |

1-02

123 | 124 | 125 | 126 |

2. 账号设置

127 | 128 | 129 | 130 |

2.1 账号设置界面

131 | 132 |

初次使用 QSunSync 时需要在此页面输入 AK/SK 进行登陆。一旦登陆成功将记录 AK/SK, 之后再次使用时,无需重新配置。

133 | 134 | 135 | 136 |

2-01

137 | 138 |

界面2 账号设置界面

139 | 140 |

2.2 账号设置

141 | 142 |

点击账号设置界面“查看我的AK & SK”或者直接输入网址portal.qiniu.com进入portal页面。

143 | 144 | 145 | 146 |

2-02

147 | 148 |

在“秘钥管理”界面可以找到AK和SK,其中SK默认隐藏,可以点击显示框中的“显示”按钮展现明文。

149 | 150 | 151 | 152 |

2-03

153 | 154 |

将对应的AK和SK填入账号设置的对应输入框中,点击“保存”即可。

155 | 156 | 157 | 158 |

3. 同步设置界面

159 | 160 |

设置好账户AK&SK之后,主界面“新建同步任务”已经变为可用状态。

161 | 162 | 163 | 164 |

3-01

165 | 166 |

点击“新建同步任务”进入任务同步设置。

167 | 168 | 169 | 170 |

3-02

171 | 172 |

界面3 同步设置界面

173 | 174 | 175 | 176 |

3.1基本设置

177 | 178 | 179 | 180 |
3.1.1 本地目录
181 | 182 |

选择需要同步到七牛的本地目录。

183 | 184 | 185 | 186 |

3-03

187 | 188 | 189 | 190 |
3.1.2 目标空间
191 | 192 |

将文件上传到七牛指定空间

193 | 194 | 195 | 196 |

3-04

197 | 198 | 199 | 200 |

3-04-x

201 | 202 | 203 | 204 |

3.2 高级设置

205 | 206 | 207 | 208 |

3-05-1

209 | 210 | 211 | 212 |

3-05-2

213 | 214 | 215 | 216 |
3.2.1 附加文件前缀
217 | 218 |

可使用前缀进行文件管理。

219 | 220 |

示例:如需要将两个文件夹 a、b 同步到同一七牛空间,其 中文件夹 a 内全为图片,有文件 1.jpg、2.jpg 等;文件夹 b 中全为视频,有文件 3.mp4、 4.mp4 等。为了方便管理,在文件夹 a 同步时,设置前缀为 img,则文件 1.jpg、2.jpg 在七 牛空间中的 key(文件名)分别为:img1.jpg 及 img2.jpg。文件夹 b 同步时,设置前缀为 video,则文件 3.mp4、4.mp4 在七牛空间中的 key 为:video3.mp4 及 video4.mp4。

221 | 222 | 223 | 224 |
3.2.2 忽略文件名相对路径
225 | 226 |

示例:假设有同步文件夹 sub1,其中 sub1 下有子文件夹 sub2,文件夹sub2下有文件夹sub3,如下图所示,其中sub3下有图片1.jpg。若勾选该选项,则此图片在七牛空间中的key为sub1/sub2/sub3/1.jpg,勾选之后的key为1.jpg。

227 | 228 | 229 | 230 |

3-06

231 | 232 | 233 | 234 |
3.2.3 同名文件覆盖
235 | 236 |

文件上传到空间时,会通过对比 hash 确认是否空间中已有相同文件。若 hash 相等,则 不会上传新文件。若 hash 不相等,则会查看“空间中同名文件强制覆盖”选项,如该选项被勾选,则会上传新的文件,若此选项不被勾选,则会报错。

237 | 238 | 239 | 240 |
3.2.4 分片上传阈值
241 | 242 |

默认值为最大4MB。设置阈值后,若文件大小大于等于该值,则自动使用分片上传。小于该值则自动使用表单上传。

243 | 244 | 245 | 246 |
3.2.5 同步的并发数
247 | 248 |

可以允许多文件同时并发上传,并发数可选范围:1~60。

249 | 250 | 251 | 252 |
3.2.6 上传目的机房
253 | 254 |

程序会根据用户的Bucket(目标空间)自动配置,可选项包括”直传”和”CDN加速”。

255 | 256 | 257 | 258 |

3-07

259 | 260 | 261 | 262 |

3.3 开始同步

263 | 264 |

完成以上所有设置后,点击“开始同步按钮”开始同步。

265 | 266 | 267 | 268 |

4. 同步信息

269 | 270 |

点击“开始同步”按钮后,如果一切正常,程序会跳转到“同步进度”页面。

271 | 272 | 273 | 274 |

4.1 同步进行时

275 | 276 | 277 | 278 |

4-01

279 | 280 |

界面4.1 同步进度界面

281 | 282 | 283 | 284 |
4.1.1 多线程同步进度表
285 | 286 |

可得到每个线程的同步文件名、当前文件上传进度及速度。

287 | 288 | 289 | 290 |
4.1.2 同步信息汇总
291 | 292 |

可得到完成同步文件个数及同步完成百分比。

293 | 294 | 295 | 296 |
4.1.3 暂停
297 | 298 |

暂停后该按钮变为“继续”,点击“继续”按钮可继续此次同步任务。若在暂停时有文件尚未传完,该文件可在“继续”后断点续传。

299 | 300 | 301 | 302 |
4.1.4 结束
303 | 304 |

点击按钮跳此次同步结束,跳转至同步完成界面(界面 4.2)。此次任务及其配置将会保存至最近同步任务。若在结束时有文件尚未传完,该文件之后可断点续传。

305 | 306 | 307 | 308 |

4.2 同步完成后

309 | 310 | 311 | 312 |

4-01

313 | 314 |

界面4.2 同步完成界面

315 | 316 | 317 | 318 |
4.2.1 同步信息总览
319 | 320 |

可得到同步时间及同步成功/失败等文件名及错误信息。

321 | 322 | 323 | 324 |
4.2.2 导出日志
325 | 326 |

可导出本次同步日志,包含同步成功及失败文件信息。

327 | 328 | 329 | 330 |
4.2.3 返回首页
331 | 332 |

跳转至初始界面(界面 1)。

333 | 334 | 335 | 336 |

5. 历史记录

337 | 338 |

回到主页后,在“最近同步任务”列表中可以看到同步历史(按最近时间排列)。选择指定记录,右键菜单中有“删除任务”(删除历史记录)和“导出日志”功能。

339 | 340 | 341 | 342 |

5-01

343 | 344 |

界面5 主界面(历史记录)

-------------------------------------------------------------------------------- /docs/imgs/1-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/1-01.png -------------------------------------------------------------------------------- /docs/imgs/1-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/1-02.png -------------------------------------------------------------------------------- /docs/imgs/2-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/2-01.png -------------------------------------------------------------------------------- /docs/imgs/2-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/2-02.png -------------------------------------------------------------------------------- /docs/imgs/2-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/2-03.png -------------------------------------------------------------------------------- /docs/imgs/3-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-01.png -------------------------------------------------------------------------------- /docs/imgs/3-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-02.png -------------------------------------------------------------------------------- /docs/imgs/3-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-03.png -------------------------------------------------------------------------------- /docs/imgs/3-04-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-04-x.png -------------------------------------------------------------------------------- /docs/imgs/3-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-04.png -------------------------------------------------------------------------------- /docs/imgs/3-05-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-05-1.png -------------------------------------------------------------------------------- /docs/imgs/3-05-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-05-2.png -------------------------------------------------------------------------------- /docs/imgs/3-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-06.png -------------------------------------------------------------------------------- /docs/imgs/3-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/3-07.png -------------------------------------------------------------------------------- /docs/imgs/4-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/4-01.png -------------------------------------------------------------------------------- /docs/imgs/4-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/4-02.png -------------------------------------------------------------------------------- /docs/imgs/5-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/imgs/5-01.png -------------------------------------------------------------------------------- /docs/qsunbox-太阳队.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/qsunbox-太阳队.pptx -------------------------------------------------------------------------------- /docs/qsunbox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/qsunbox.jpg -------------------------------------------------------------------------------- /docs/七牛云文件同步工具v1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/七牛云文件同步工具v1.pdf -------------------------------------------------------------------------------- /docs/七牛云文件同步工具文档.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/docs/七牛云文件同步工具文档.pdf -------------------------------------------------------------------------------- /lib/System.Data.SQLite.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/QSunSync/8e2e62fddbc7f5df6ef8e98077c06d046ff39123/lib/System.Data.SQLite.dll --------------------------------------------------------------------------------