/* * * (c) Copyright Ascensio System Limited 2010-2018 * * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. * * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html * * You can contact Ascensio System SIA by email at sales@onlyoffice.com * * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. * * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains * relevant author attributions when distributing the software. If the display of the logo in its graphic * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" * in every copy of the program you distribute. * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. * */ #region Import using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.IO; using System.Web; using ASC.Common.Logging; using ASC.Data.Storage.Configuration; using SelectelSharp; using MimeMapping = ASC.Common.Web.MimeMapping; #endregion namespace ASC.Data.Storage.Selectel { public class SelectelStorage : BaseStorage { private readonly Dictionary _domainsAcl; private readonly ACL _moduleAcl; private String _authUser; private String _authPwd; private String _private_container; private String _public_container; private String _subDir; private Uri _cname; private Uri _cnameSSL; private bool _lowerCasing = true; private static readonly ILog _logger = LogManager.GetLogger("ASC.Data.Storage.Selectel.SelectelStorage"); public SelectelStorage(String tenant) { _tenant = tenant; _modulename = string.Empty; _dataList = null; _domainsExpires = new Dictionary {{string.Empty, TimeSpan.Zero}}; _domainsAcl = new Dictionary(); _moduleAcl = ACL.Auto; } public SelectelStorage(String tenant, HandlerConfigurationElement handlerConfig, ModuleConfigurationElement moduleConfig) { _tenant = tenant; _modulename = moduleConfig.Name; _dataList = new DataList(moduleConfig); _domainsExpires = moduleConfig.Domains.Cast() .Where(x => x.Expires != TimeSpan.Zero) .ToDictionary(x => x.Name, y => y.Expires); _domainsExpires.Add(String.Empty, moduleConfig.Expires); _domainsAcl = moduleConfig.Domains.Cast().ToDictionary(x => x.Name, y => y.Acl); _moduleAcl = moduleConfig.Acl; } public override IDataStore Configure(IDictionary props) { _authUser = props["authUser"]; _authPwd = props["authPwd"]; _public_container = props["public_container"]; _private_container = !string.IsNullOrEmpty(props["private_container"]) ? props["private_container"] : _public_container; if (string.IsNullOrEmpty(_public_container)) throw new ArgumentException("_public_container"); if (props.ContainsKey("lower")) { bool.TryParse(props["lower"], out _lowerCasing); } if (props.ContainsKey("subdir")) { _subDir = props["subdir"]; } var client = GetClient().Result; _cname = props.ContainsKey("cname") && Uri.IsWellFormedUriString(props["cname"], UriKind.Absolute) ? new Uri(props["cname"], UriKind.Absolute) : new Uri(client.StorageUrl, UriKind.Absolute); _cnameSSL = props.ContainsKey("cnamessl") && Uri.IsWellFormedUriString(props["cnamessl"], UriKind.Absolute) ? new Uri(props["cnamessl"], UriKind.Absolute) : new Uri(client.StorageUrl, UriKind.Absolute); return this; } private async Task GetClient() { var client = new SelectelClient(); await client.AuthorizeAsync(_authUser, _authPwd); return client; } private string MakePath(string domain, string path) { string result; path = path.TrimStart('\\', '/').TrimEnd('/').Replace('\\', '/'); if (!String.IsNullOrEmpty(_subDir)) { if (_subDir.Length == 1 && (_subDir[0] == '/' || _subDir[0] == '\\')) result = path; else result = String.Format("{0}/{1}", _subDir.TrimEnd('/'), path); // Ignory all, if _subDir is not null } else//Key combined from module+domain+filename result = string.Format("{0}/{1}{2}{3}", _tenant, string.IsNullOrEmpty(_modulename) ? "" : _modulename + "/", string.IsNullOrEmpty(domain) ? "" : domain + "/", path); result = result.Replace("//", "/").TrimStart('/'); if (_lowerCasing) { result = result.ToLowerInvariant(); } return result; } public override Uri GetInternalUri(string domain, string path, TimeSpan expire, IEnumerable headers) { if (expire == TimeSpan.Zero || expire == TimeSpan.MinValue || expire == TimeSpan.MaxValue) { expire = GetExpire(domain); } if (expire == TimeSpan.Zero || expire == TimeSpan.MinValue || expire == TimeSpan.MaxValue) { return GetUriShared(domain, path); } var client = GetClient().Result; return client.GetPreSignUriAsync(_private_container, MakePath(domain, path), expire).Result; } private Uri GetUriShared(string domain, string path) { return new Uri(String.Format("{0}{1}/{2}", SecureHelper.IsSecure() ? _cnameSSL : _cname, _public_container, MakePath(domain, path))); } public override System.IO.Stream GetReadStream(string domain, string path) { return GetReadStream(domain, path, 0); } public override System.IO.Stream GetReadStream(string domain, string path, int offset) { var client = GetClient().Result; var file = client.GetFileAsync(_private_container, MakePath(domain, path), null, false).Result; if (file == null) return null; var responseStream = file.ResponseStream; Stream fileStream = responseStream; if (offset > 0) { if (!responseStream.CanSeek) { fileStream = responseStream.GetBuffered(); responseStream.Close(); } fileStream.Seek(offset, SeekOrigin.Begin); } return fileStream; } public override Uri Save(string domain, string path, System.IO.Stream stream) { return Save(domain, path, stream, null, null, ACL.Auto, null); } public override Uri Save(string domain, string path, System.IO.Stream stream, Configuration.ACL acl) { return Save(domain, path, stream, null, null, acl); } protected override Uri SaveWithAutoAttachment(string domain, string path, System.IO.Stream stream, string attachmentFileName) { var contentDisposition = string.Format("attachment; filename={0};", HttpUtility.UrlPathEncode(attachmentFileName)); if (attachmentFileName.Any(c => (int)c >= 0 && (int)c <= 127)) { contentDisposition = string.Format("attachment; filename*=utf-8''{0};", HttpUtility.UrlPathEncode(attachmentFileName)); } return Save(domain, path, stream, null, null, ACL.Auto, null); } public override Uri Save(string domain, string path, System.IO.Stream stream, string contentType, string contentDisposition) { return Save(domain, path, stream, contentType, contentDisposition, ACL.Auto, null); } public override Uri Save(string domain, string path, System.IO.Stream stream, string contentEncoding, int cacheDays) { return Save(domain, path, stream, null, null, ACL.Auto, contentEncoding, cacheDays); } public Uri Save(string domain, string path, Stream stream, string contentType, string contentDisposition, ACL acl, string contentEncoding = null, int cacheDays = 5, DateTime? deleteAt = null, long? deleteAfter = null) { var client = GetClient().Result; var buffered = stream.GetBuffered(); if (QuotaController != null) { QuotaController.QuotaUsedCheck(buffered.Length); } var mime = string.IsNullOrEmpty(contentType) ? MimeMapping.GetMimeMapping(Path.GetFileName(path)) : contentType; if (mime == "application/octet-stream") { contentDisposition = "attachment"; } var customHeaders = new Dictionary(); if (cacheDays > 0) { customHeaders.Add("Cache-Control", String.Format("public, maxage={0}", (int)TimeSpan.FromDays(cacheDays).TotalSeconds)); customHeaders.Add("Expires", DateTime.UtcNow.Add(TimeSpan.FromDays(cacheDays))); } if (!String.IsNullOrEmpty(contentEncoding)) customHeaders.Add("Content-Encoding", contentEncoding); client.UploadFileAsync(_private_container, MakePath(domain, path), false, true, buffered, null, contentDisposition, mime, deleteAt, deleteAfter, customHeaders).Wait(); var cannedACL = acl == ACL.Auto ? GetDomainACL(domain) : ACL.Read; if (cannedACL == ACL.Read) { var createSymLinkStatus = client.CreateSymLink( _public_container, MakePath(domain, path), SelectelSharp.Models.Link.Symlink.SymlinkType.Symlink, String.Format("/{0}/{1}/", _private_container, MakePath(domain, path))).Result; if (!createSymLinkStatus) { _logger.ErrorFormat("Symlink '{0}; not created", _public_container + "/" + MakePath(domain, path)); throw new Exception(String.Format("Symlink '{0}; not created", _public_container + "/" + MakePath(domain, path))); } try { var invalidationResult = client.CDNIvalidation(_public_container, new[] { MakePath(domain, path) }).Result; } catch (Exception exp) { _logger.InfoFormat("The invalidation {0} failed", _public_container + "/" + MakePath(domain, path)); _logger.Error(exp); } } QuotaUsedAdd(domain, buffered.Length); return GetUri(domain, path); } private ACL GetDomainACL(string domain) { if (GetExpire(domain) != TimeSpan.Zero) { return ACL.Auto; } if (_domainsAcl.ContainsKey(domain)) { return _domainsAcl[domain]; } return _moduleAcl; } public override void Delete(string domain, string path) { var client = GetClient().Result; var key = MakePath(domain, path); var size = GetFileSize(domain, path); client.DeleteFileAsync(_private_container, MakePath(domain, path)).Wait(); QuotaUsedDelete(domain, size); } public override void DeleteFiles(string domain, string folderPath, string pattern, bool recursive) { var client = GetClient().Result; var files = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, MakePath(domain, folderPath), null, null) .Result .Where(x => Wildcard.IsMatch(pattern, Path.GetFileName(x.Name))); if (!files.Any()) return; files.ToList().ForEach(x => client.DeleteFileAsync(_private_container, x.Name).Wait()); if (QuotaController != null) { QuotaUsedDelete(domain, files.Select(x => x.Bytes).Sum()); } } public override void DeleteFiles(string domain, List paths) { if (!paths.Any()) return; var keysToDel = new List(); long quotaUsed = 0; foreach (var path in paths) { try { var key = MakePath(domain, path); if (QuotaController != null) { quotaUsed += GetFileSize(domain, path); } keysToDel.Add(key); } catch (FileNotFoundException) { } } if (!keysToDel.Any()) return; var client = GetClient().Result; keysToDel.ForEach(x => client.DeleteFileAsync(_private_container, x).Wait()); if (quotaUsed > 0) { QuotaUsedDelete(domain, quotaUsed); } } public override void DeleteFiles(string domain, string folderPath, DateTime fromDate, DateTime toDate) { var client = GetClient().Result; var files = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, MakePath(domain, folderPath), null, null) .Result .Where(x => x.Date >= fromDate && x.Date <= toDate); if (!files.Any()) return; files.ToList().ForEach(x => client.DeleteFileAsync(_private_container, x.Name).Wait()); if (QuotaController != null) { QuotaUsedDelete(domain, files.Select(x => x.Bytes).Sum()); } } public override void MoveDirectory(string srcdomain, string srcdir, string newdomain, string newdir) { var client = GetClient().Result; var srckey = MakePath(srcdomain, srcdir); var dstkey = MakePath(newdomain, newdir); var paths = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, srckey).Result.Select(x => x.Name); foreach (var path in paths) { client.CopyFileAsync(_private_container, path, _private_container, path.Replace(srckey, dstkey)).Wait(); client.DeleteFileAsync(_private_container, path).Wait(); } } public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath) { var srcKey = MakePath(srcdomain, srcpath); var dstKey = MakePath(newdomain, newpath); var size = GetFileSize(srcdomain, srcpath); var client = GetClient().Result; client.CopyFileAsync(_private_container, srcKey, _private_container, dstKey).Wait(); Delete(srcdomain, srcpath); QuotaUsedDelete(srcdomain, size); QuotaUsedAdd(newdomain, size); return GetUri(newdomain, newpath); } public override Uri SaveTemp(string domain, out string assignedPath, System.IO.Stream stream) { assignedPath = Guid.NewGuid().ToString(); return Save(domain, assignedPath, stream); } public override string[] ListDirectoriesRelative(string domain, string path, bool recursive) { var client = GetClient().Result; return client.GetContainerFilesAsync(_private_container, int.MaxValue, null, path) .Result .Select(x => x.Name.Substring(MakePath(domain, path + "/").Length)).ToArray(); } public override string[] ListFilesRelative(string domain, string path, string pattern, bool recursive) { var paths = new List(); var client = GetClient().Result; if (recursive) { paths = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, MakePath(domain, path)).Result.Select(x => x.Name).ToList(); } else { paths = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, MakePath(domain, path)).Result.Select(x => x.Name).ToList(); } return paths .Where(x => Wildcard.IsMatch(pattern, Path.GetFileName(x))) .Select(x => x.Substring(MakePath(domain, path + "/").Length).TrimStart('/')).ToArray(); } public override bool IsFile(string domain, string path) { var client = GetClient().Result; var files = client.GetContainerFilesAsync(_private_container, 2, null, MakePath(domain, path), null, null).Result; if (files == null) return false; return files.Count() > 0; } public override bool IsDirectory(string domain, string path) { return IsFile(domain, path); } public override void DeleteDirectory(string domain, string path) { var client = GetClient().Result; var files = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, MakePath(domain, path), null, null).Result; if (!files.Any()) return; files.ForEach(x => client.DeleteFileAsync(_private_container, x.Name).Wait()); if (QuotaController != null) { QuotaUsedDelete(domain, files.Select(x => x.Bytes).Sum()); } } public override long GetFileSize(string domain, string path) { var client = GetClient().Result; var fileInfos = client.GetContainerFilesAsync(_private_container, 1, null, MakePath(domain, path), null, null).Result; if (!fileInfos.Any()) throw new FileNotFoundException(); return fileInfos.Single().Bytes; } public override long GetDirectorySize(string domain, string path) { var client = GetClient().Result; var fileInfos = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, MakePath(domain, path), null, null).Result; long quotaUsed = 0; foreach (var info in fileInfos) { quotaUsed += info.Bytes; } return quotaUsed; } public override long ResetQuota(string domain) { if (QuotaController != null) { var client = GetClient().Result; var files = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, domain).Result; if (files == null) return 0; var size = files.Select(x => x.Bytes).Sum(); QuotaController.QuotaUsedSet(_modulename, domain, _dataList.GetData(domain), size); return size; } return 0; } public override long GetUsedQuota(string domain) { var client = GetClient().Result; var files = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, MakePath(domain, String.Empty)).Result; if (files == null) return 0; var size = files.Select(x => x.Bytes).Sum(); return size; } public override Uri Copy(string srcdomain, string path, string newdomain, string newpath) { var srcKey = MakePath(srcdomain, path); var dstKey = MakePath(newdomain, newpath); var size = GetFileSize(srcdomain, path); var client = GetClient().Result; client.CopyFileAsync(_private_container, srcKey, _private_container, dstKey).Wait(); QuotaUsedAdd(newdomain, size); return GetUri(newdomain, newpath); } public override void CopyDirectory(string srcdomain, string dir, string newdomain, string newdir) { var srckey = MakePath(srcdomain, dir); var dstkey = MakePath(newdomain, newdir); var client = GetClient().Result; var files = client.GetContainerFilesAsync(_private_container, int.MaxValue, null, srckey).Result; foreach (var file in files) { client.CopyFileAsync(_private_container, file.Name, _private_container, file.Name.Replace(srckey, dstkey)).Wait(); QuotaUsedAdd(newdomain, file.Bytes); } } public override string SavePrivate(string domain, string path, System.IO.Stream stream, DateTime expires) { var uri = Save(domain, path, stream, "application/octet-stream", "attachment", ACL.Auto, null, 5, expires); return uri.ToString(); } public override void DeleteExpired(string domain, string path, TimeSpan oldThreshold) { // selectel run automatically deleting files } public override string GetUploadForm(string domain, string directoryPath, string redirectTo, long maxUploadSize, string contentType, string contentDisposition, string submitLabel) { throw new NotImplementedException(); } public override string GetUploadedUrl(string domain, string directoryPath) { throw new NotImplementedException(); } public override string GetUploadUrl() { throw new NotImplementedException(); } public override string GetPostParams(string domain, string directoryPath, long maxUploadSize, string contentType, string contentDisposition) { throw new NotImplementedException(); } #region chunking public override string InitiateChunkedUpload(string domain, string path) { return Path.GetTempFileName(); } public override string UploadChunk(string domain, string path, string filePath, Stream stream, long defaultChunkSize, int chunkNumber, long chunkLength) { int BufferSize = 8192; var mode = chunkNumber == 0 ? FileMode.Create : FileMode.Append; using (var fs = new FileStream(filePath, mode)) { var buffer = new byte[BufferSize]; int readed; while ((readed = stream.Read(buffer, 0, BufferSize)) != 0) { fs.Write(buffer, 0, readed); } } return string.Format("{0}_{1}", chunkNumber, filePath); } public override void AbortChunkedUpload(string domain, string path, string filePath) { if (File.Exists(filePath)) { File.Delete(filePath); } } public override Uri FinalizeChunkedUpload(string domain, string path, string filePath, Dictionary eTags) { var stream = new FileStream(filePath, FileMode.Open); var client = GetClient().Result; client.UploadFileAsync(_private_container, MakePath(domain, path), true, true,stream).Wait(); if (File.Exists(filePath)) { File.Delete(filePath); } if (QuotaController != null) { var size = GetFileSize(domain, path); QuotaUsedAdd(domain, size); } return GetUri(domain, path); } public override bool IsSupportChunking { get { return true; } } #endregion } }