|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Security.AccessControl;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace ZeroLevel.Services.FileSystem
|
|
|
|
|
{
|
|
|
|
|
internal abstract class StoreTask
|
|
|
|
|
{
|
|
|
|
|
protected const int DEFAULT_STREAM_BUFFER_SIZE = 16384;
|
|
|
|
|
|
|
|
|
|
public async Task Store()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await StoreImpl().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.SystemError(ex, "Fault store to archive");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static async Task TransferAsync(Stream input, Stream output)
|
|
|
|
|
{
|
|
|
|
|
if (input.CanRead == false)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Input stream can not be read.");
|
|
|
|
|
}
|
|
|
|
|
if (output.CanWrite == false)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Output stream can not be write.");
|
|
|
|
|
}
|
|
|
|
|
var readed = 0;
|
|
|
|
|
var buffer = new byte[DEFAULT_STREAM_BUFFER_SIZE];
|
|
|
|
|
while ((readed = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
|
|
|
|
|
{
|
|
|
|
|
output.Write(buffer, 0, readed);
|
|
|
|
|
}
|
|
|
|
|
output.Flush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static Stream GenerateStreamFromString(string s)
|
|
|
|
|
{
|
|
|
|
|
MemoryStream stream = new MemoryStream();
|
|
|
|
|
StreamWriter writer = new StreamWriter(stream);
|
|
|
|
|
writer.Write(s);
|
|
|
|
|
writer.Flush();
|
|
|
|
|
stream.Position = 0;
|
|
|
|
|
return stream;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract Task StoreImpl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class StoreText :
|
|
|
|
|
StoreTask
|
|
|
|
|
{
|
|
|
|
|
private readonly string _text;
|
|
|
|
|
private readonly string _path;
|
|
|
|
|
|
|
|
|
|
public StoreText(string text, string path)
|
|
|
|
|
{
|
|
|
|
|
_text = text;
|
|
|
|
|
_path = path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async Task StoreImpl()
|
|
|
|
|
{
|
|
|
|
|
using (var input_stream = GenerateStreamFromString(_text))
|
|
|
|
|
{
|
|
|
|
|
using (var out_stream = File.Create(_path))
|
|
|
|
|
{
|
|
|
|
|
await TransferAsync(input_stream, out_stream).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class StoreData :
|
|
|
|
|
StoreTask
|
|
|
|
|
{
|
|
|
|
|
private readonly byte[] _data;
|
|
|
|
|
private readonly string _path;
|
|
|
|
|
|
|
|
|
|
public StoreData(byte[] data, string path)
|
|
|
|
|
{
|
|
|
|
|
_data = data;
|
|
|
|
|
_path = path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async Task StoreImpl()
|
|
|
|
|
{
|
|
|
|
|
using (var input_stream = new MemoryStream(_data))
|
|
|
|
|
{
|
|
|
|
|
using (var out_stream = File.Create(_path,
|
|
|
|
|
DEFAULT_STREAM_BUFFER_SIZE,
|
|
|
|
|
FileOptions.Asynchronous))
|
|
|
|
|
{
|
|
|
|
|
await TransferAsync(input_stream, out_stream).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class StoreStream :
|
|
|
|
|
StoreTask
|
|
|
|
|
{
|
|
|
|
|
private readonly Stream _stream;
|
|
|
|
|
private readonly string _path;
|
|
|
|
|
|
|
|
|
|
public StoreStream(Stream stream, string path)
|
|
|
|
|
{
|
|
|
|
|
_stream = stream;
|
|
|
|
|
_path = path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async Task StoreImpl()
|
|
|
|
|
{
|
|
|
|
|
using (_stream)
|
|
|
|
|
{
|
|
|
|
|
using (var out_stream = File.Create(_path,
|
|
|
|
|
DEFAULT_STREAM_BUFFER_SIZE,
|
|
|
|
|
FileOptions.Asynchronous))
|
|
|
|
|
{
|
|
|
|
|
await TransferAsync(_stream, out_stream).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class StoreFile :
|
|
|
|
|
StoreTask
|
|
|
|
|
{
|
|
|
|
|
private readonly string _input_path;
|
|
|
|
|
private readonly string _path;
|
|
|
|
|
|
|
|
|
|
public StoreFile(string input_file_path, string path)
|
|
|
|
|
{
|
|
|
|
|
_input_path = input_file_path;
|
|
|
|
|
_path = path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async Task StoreImpl()
|
|
|
|
|
{
|
|
|
|
|
using (var input_stream = File.Open(_input_path, FileMode.Open,
|
|
|
|
|
FileAccess.Read, FileShare.Read))
|
|
|
|
|
{
|
|
|
|
|
using (var out_stream = File.Create(_path,
|
|
|
|
|
DEFAULT_STREAM_BUFFER_SIZE,
|
|
|
|
|
FileOptions.Asynchronous))
|
|
|
|
|
{
|
|
|
|
|
await TransferAsync(input_stream, out_stream).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Файловый архив
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class FileArchive :
|
|
|
|
|
IDisposable
|
|
|
|
|
{
|
|
|
|
|
private static int _counter;
|
|
|
|
|
private static string NextCounter
|
|
|
|
|
{ get { return Interlocked.Increment(ref _counter).ToString("X8"); } }
|
|
|
|
|
private readonly string _base_path;
|
|
|
|
|
private const string DIRECTORY_NAME_TEMPLATE = "yyyyMMdd";
|
|
|
|
|
private const string FILE_NAME_TEMPLATE = "HH_mm_ss_{0}{1}";
|
|
|
|
|
private readonly string _extension;
|
|
|
|
|
private readonly bool _override = false;
|
|
|
|
|
private readonly ConcurrentQueue<StoreTask> _tasks = new ConcurrentQueue<StoreTask>();
|
|
|
|
|
private volatile bool _disposed = false;
|
|
|
|
|
private volatile bool _stopped = false;
|
|
|
|
|
private string _currentArchivePath;
|
|
|
|
|
private bool _split_by_date = false;
|
|
|
|
|
|
|
|
|
|
public FileArchive(string base_path,
|
|
|
|
|
string ext,
|
|
|
|
|
bool split_by_date,
|
|
|
|
|
bool override_if_exists)
|
|
|
|
|
{
|
|
|
|
|
_base_path = PreparePath(base_path);
|
|
|
|
|
_extension = ext;
|
|
|
|
|
_override = override_if_exists;
|
|
|
|
|
_split_by_date = split_by_date;
|
|
|
|
|
RenewArchivePath();
|
|
|
|
|
if (_split_by_date)
|
|
|
|
|
{
|
|
|
|
|
Sheduller.RemindEveryNonlinearPeriod(
|
|
|
|
|
() => (DateTime.Now.AddDays(1).Date - DateTime.Now).Add(TimeSpan.FromMilliseconds(100)),
|
|
|
|
|
RenewArchivePath);
|
|
|
|
|
}
|
|
|
|
|
Task.Run(Consume);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RenewArchivePath()
|
|
|
|
|
{
|
|
|
|
|
if (_split_by_date)
|
|
|
|
|
{
|
|
|
|
|
var directory_name = DateTime.Now.ToString(DIRECTORY_NAME_TEMPLATE);
|
|
|
|
|
var directory_path = Path.Combine(_base_path, directory_name);
|
|
|
|
|
PrepareFolder(directory_path);
|
|
|
|
|
_currentArchivePath = directory_path;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var directory_path = _base_path;
|
|
|
|
|
PrepareFolder(directory_path);
|
|
|
|
|
_currentArchivePath = directory_path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task Consume()
|
|
|
|
|
{
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
StoreTask result;
|
|
|
|
|
while (_tasks.TryDequeue(out result))
|
|
|
|
|
{
|
|
|
|
|
await result.Store().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
Thread.Sleep(100);
|
|
|
|
|
} while (_disposed == false);
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Save text to archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="text">Text</param>
|
|
|
|
|
/// <param name="subfolder_name">Archive file name (HH_mm_ss_fff_counter.{ext} by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void StoreText(string text, string subfolder_name = null, string file_name = null)
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreText(text, CreateArchiveFilePath(subfolder_name, file_name)));
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Saving the specified file to the archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="file_path">File path</param>
|
|
|
|
|
/// <param name="subfolder_name">Archive file name (original file name by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void Store(string file_path, string subfolder_name = null, string file_name = null)
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreFile(file_path, CreateArchiveFilePath(subfolder_name, file_name)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Store(string file_path, bool immediate, string subfolder_name = null, string file_name = null)
|
|
|
|
|
{
|
|
|
|
|
if (immediate)
|
|
|
|
|
{
|
|
|
|
|
new StoreFile(file_path, CreateArchiveFilePath(subfolder_name, file_name)).Store().Wait();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreFile(file_path, CreateArchiveFilePath(subfolder_name, file_name)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Saving data from stream to archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="stream">Data stream for reading</param>
|
|
|
|
|
/// <param name="subfolder_name">Archive file name (HH_mm_ss_fff_counter.{ext} by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void Store(Stream stream, string subfolder_name = null, string file_name = null)
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreStream(stream, CreateArchiveFilePath(subfolder_name, file_name)));
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Saving data in binary form in the archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data">Data</param>
|
|
|
|
|
/// <param name="subfolder_name">Archive file name (HH_mm_ss_fff_counter.{ext} by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void StoreData(byte[] data, string subfolder_name = null, string file_name = null)
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreData(data, CreateArchiveFilePath(subfolder_name, file_name)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Apply(StoreTask task)
|
|
|
|
|
{
|
|
|
|
|
if (_stopped == false)
|
|
|
|
|
{
|
|
|
|
|
_tasks.Enqueue(task);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Helpers
|
|
|
|
|
private string CreateArchiveFilePath(string subfolder_name, string filename)
|
|
|
|
|
{
|
|
|
|
|
string archive_file_path;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(subfolder_name))
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(filename))
|
|
|
|
|
{
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
archive_file_path = Path.Combine(_currentArchivePath,
|
|
|
|
|
string.Format(DateTime.Now.ToString(FILE_NAME_TEMPLATE),
|
|
|
|
|
NextCounter, _extension));
|
|
|
|
|
} while (_override == false && File.Exists(archive_file_path));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
archive_file_path = Path.Combine(_currentArchivePath, filename);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var base_path = Path.Combine(_currentArchivePath, subfolder_name);
|
|
|
|
|
PrepareFolder(base_path);
|
|
|
|
|
if (string.IsNullOrWhiteSpace(filename))
|
|
|
|
|
{
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
archive_file_path = Path.Combine(base_path,
|
|
|
|
|
string.Format(DateTime.Now.ToString(FILE_NAME_TEMPLATE),
|
|
|
|
|
NextCounter, _extension));
|
|
|
|
|
} while (_override == false && File.Exists(archive_file_path));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
archive_file_path = Path.Combine(base_path, filename);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return archive_file_path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void PrepareFolder(string path)
|
|
|
|
|
{
|
|
|
|
|
if (Directory.Exists(path) == false)
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(path);
|
|
|
|
|
FSUtils.SetupFolderPermission(path,$"{Environment.UserDomainName}\\{Environment.UserName}",
|
|
|
|
|
FileSystemRights.Write | FileSystemRights.Read | FileSystemRights.Delete | FileSystemRights.Modify,
|
|
|
|
|
AccessControlType.Allow);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string PreparePath(string path)
|
|
|
|
|
{
|
|
|
|
|
if (path.IndexOf(':') == -1)
|
|
|
|
|
{
|
|
|
|
|
path = Path.Combine(Configuration.BaseDirectory, path);
|
|
|
|
|
}
|
|
|
|
|
return FSUtils.PathCorrection(path);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
_stopped = true;
|
|
|
|
|
while (_tasks.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
Thread.Sleep(50);
|
|
|
|
|
}
|
|
|
|
|
_disposed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class FileBuffer :
|
|
|
|
|
IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly string _base_path;
|
|
|
|
|
private readonly BlockingCollection<StoreTask> _tasks = new BlockingCollection<StoreTask>();
|
|
|
|
|
private volatile bool _disposed = false;
|
|
|
|
|
private volatile bool _stopped = false;
|
|
|
|
|
private string _currentArchivePath;
|
|
|
|
|
private Thread _storeThread;
|
|
|
|
|
|
|
|
|
|
public FileBuffer(string base_path)
|
|
|
|
|
{
|
|
|
|
|
_base_path = PreparePath(base_path);
|
|
|
|
|
var directory_path = _base_path;
|
|
|
|
|
PrepareFolder(directory_path);
|
|
|
|
|
_currentArchivePath = directory_path;
|
|
|
|
|
_storeThread = new Thread(Consume);
|
|
|
|
|
_storeThread.IsBackground = true;
|
|
|
|
|
_storeThread.Start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Consume()
|
|
|
|
|
{
|
|
|
|
|
StoreTask result;
|
|
|
|
|
while (_disposed == false)
|
|
|
|
|
{
|
|
|
|
|
result = _tasks.Take();
|
|
|
|
|
result.Store().ContinueWith(t =>
|
|
|
|
|
{
|
|
|
|
|
if (t.IsFaulted)
|
|
|
|
|
{
|
|
|
|
|
Log.SystemError(t.Exception, "[FileBuffer] Fault store file");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Save text to archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="text">Text</param>
|
|
|
|
|
/// <param name="name">Archive file name (HH_mm_ss_fff_counter.{ext} by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void StoreText(string text, string name = null)
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreText(text, CreateArchiveFilePath(name)));
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Saving the specified file to the archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="file_path">File path</param>
|
|
|
|
|
/// <param name="subfolder_name">Archive file name (original file name by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void Store(string file_path, string name = null)
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreFile(file_path, CreateArchiveFilePath(name)));
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Sync saving the specified file to the archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="file_path">File path</param>
|
|
|
|
|
/// <param name="subfolder_name">Archive file name (original file name by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void Store(string file_path, bool immediate, string name = null)
|
|
|
|
|
{
|
|
|
|
|
if (immediate)
|
|
|
|
|
{
|
|
|
|
|
new StoreFile(file_path, CreateArchiveFilePath(name)).Store().Wait();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreFile(file_path, CreateArchiveFilePath(name)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Saving data from stream to archive
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="stream">Data stream for reading</param>
|
|
|
|
|
/// <param name="name">Archive file name (HH_mm_ss_fff_counter.{ext} by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void Store(Stream stream, string name = null)
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreStream(stream, CreateArchiveFilePath(name)));
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Сохранение данных в бинарном виде в архив
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data">Data</param>
|
|
|
|
|
/// <param name="name">Archive file name (HH_mm_ss_fff_counter.{ext} by default)</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public void StoreData(byte[] data, string name = null)
|
|
|
|
|
{
|
|
|
|
|
Apply(new StoreData(data, CreateArchiveFilePath(name)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Apply(StoreTask task)
|
|
|
|
|
{
|
|
|
|
|
if (_stopped == false)
|
|
|
|
|
{
|
|
|
|
|
_tasks.Add(task);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Helpers
|
|
|
|
|
private string CreateArchiveFilePath(string original_name)
|
|
|
|
|
{
|
|
|
|
|
return Path.Combine(_currentArchivePath, FSUtils.FileNameCorrection(original_name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void PrepareFolder(string path)
|
|
|
|
|
{
|
|
|
|
|
if (Directory.Exists(path) == false)
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(path);
|
|
|
|
|
FSUtils.SetupFolderPermission(path,
|
|
|
|
|
$"{Environment.UserDomainName}\\{Environment.UserName}",
|
|
|
|
|
FileSystemRights.Write | FileSystemRights.Read | FileSystemRights.Delete | FileSystemRights.Modify,
|
|
|
|
|
AccessControlType.Allow);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string PreparePath(string path)
|
|
|
|
|
{
|
|
|
|
|
if (path.IndexOf(':') == -1)
|
|
|
|
|
{
|
|
|
|
|
path = Path.Combine(Configuration.BaseDirectory, path);
|
|
|
|
|
}
|
|
|
|
|
return FSUtils.PathCorrection(path);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
_stopped = true;
|
|
|
|
|
_tasks.CompleteAdding();
|
|
|
|
|
while (_tasks.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
Thread.Sleep(50);
|
|
|
|
|
}
|
|
|
|
|
_disposed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|