­TwitterDrive – революционное онлайн-хранилище

Опубликовано 1 апреля 2009 в 12:05 | Coding4Fun

tDrive_3

В этой статье Брайан Пик (Brian Peek) описывает TwitterDrive, приложение, использующее службу сообщений Twitter для хранения файлов.

Брайан Пик (Brian Peek (EN))

ASPSOFT, Inc. (EN)

Сложность: средняя.

Необходимое время: 2-3 часа.

Цена : бесплатно.

ПО : Visual C# Express 2008 SP1, Visual Basic Express 2008 SP1 или Visual Studio 2008 SP1, .NET Framework 3.5 SP1

Оборудование: нет.

Загрузки: приложение, исходный текст.

Дискуссионный форум : здесь (EN).

Введение

Как вы уже, наверное, догадались, TwitterDrive — это мой первоапрельский подарок Coding4Fun в этом году. Хотя это приложение действительно работает, ограничения, накладываемые Twitter, лишают его практической ценности. Однако польза от него все же есть: в данном описании рассказывается об использовании Twitter API, LINQ to XML, потоков и множества других вещей.

Как это работает

Не очень-то хорошо, на самом деле. Идея следующая: берем файл, сжимаем его, кодируем в uuencode/base64 (представляя двоичные данные в виде текста), а затем отправляем на Twitter в виде последовательности 140-символьных сообщений. После завершения загрузки файла записываем его индекс, который потребуется для последующего извлечения это файла.

Twitter

Давайте начнем с Twitter API. Документация по этому API лежит на https://apiwiki.twitter.com/ (EN).

Для TwitterDrive требуется лишь малая доля всех имеющихся функций. Нам нужна возможность передавать новое сообщение, получать временную шкалу пользователя, уничтожать сообщения и проверять учетные записи пользователей. Все это обеспечивает класс TwitterService.

В его основе лежат два метода: GetTwitter и PostTwitter. GetTwitter реализует запрос GET к Twitter по указанному URL, а PostTwitter выполняет запрос POST к Twitter по указанному URL с предоставленными данными.

C#

    1: private XDocument GetTwitter(string url)
    2: {
    3:     WebClient wc = new WebClient();
    4:  
    5:     // аутентифицируется только при наличии пароля
    6:     if(!string.IsNullOrEmpty(Password))
    7:         wc.Credentials = new NetworkCredential(Username, Password);
    8:  
    9:     Stream s = wc.OpenRead(url);
   10:  
   11:     // возвращаем XDocument для LINQ
   12:     XmlTextReader xmlReader = new XmlTextReader(s);
   13:     XDocument xdoc = XDocument.Load(xmlReader);
   14:     xmlReader.Close();
   15:     return xdoc;
   16: }
   17:  
   18: private XDocument PostTwitter(string url, string data)
   19: {
   20:     byte[] bytes = Encoding.ASCII.GetBytes(data);
   21:  
   22:     HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
   23:     request.Method = "POST";
   24:  
   25:     // если мы записываем, необходимо аутентифицироваться
   26:     request.Credentials = new NetworkCredential(Username, Password);
   27:  
   28:     // при значении 'true' Twitter вылетает
   29:     request.ServicePoint.Expect100Continue = false;
   30:     request.ContentType = "application/x-www-form-urlencoded";
   31:     request.ContentLength = bytes.Length;
   32:  
   33:     Stream reqStream = request.GetRequestStream();
   34:     reqStream.Write(bytes, 0, bytes.Length);
   35:  
   36:     // переводим ответ в XDocument для использования с LINQ
   37:     HttpWebResponse resp = (HttpWebResponse)request.GetResponse();
   38:     XmlReader xmlReader = XmlReader.Create(resp.GetResponseStream());
   39:     XDocument xdoc = XDocument.Load(xmlReader);
   40:     xmlReader.Close();
   41:     return xdoc;
   42: }

 

VB

    1: Private Function GetTwitter(ByVal url As String) As XDocument
    2:     Dim wc As New WebClient()
    3:  
    4:     ' аутентифицируется только при наличии пароля
    5:     If (Not String.IsNullOrEmpty(Password)) Then
    6:         wc.Credentials = New NetworkCredential(Username, Password)
    7:     End If
    8:  
    9:     Dim s As Stream = wc.OpenRead(url)
   10:  
   11:     ' возвращаем XDocument для LINQ
   12:     Dim xmlReader As New XmlTextReader(s)
   13:     Dim xdoc As XDocument = XDocument.Load(xmlReader)
   14:     xmlReader.Close()
   15:     Return xdoc
   16: End Function
   17:  
   18: Private Function PostTwitter(ByVal url As String, ByVal data As String) As XDocument
   19:     Dim bytes() As Byte = Encoding.ASCII.GetBytes(data)
   20:  
   21:     Dim request As HttpWebRequest = CType(WebRequest.Create(url), HttpWebRequest)
   22:     request.Method = "POST"
   23:  
   24:     ' если мы записываем, необходимо аутентифицироваться
   25:     request.Credentials = New NetworkCredential(Username, Password)
   26:  
   27:     ' при значении 'true' Twitter вылетает
   28:     request.ServicePoint.Expect100Continue = False
   29:     request.ContentType = "application/x-www-form-urlencoded"
   30:     request.ContentLength = bytes.Length
   31:  
   32:     Dim reqStream As Stream = request.GetRequestStream()
   33:     reqStream.Write(bytes, 0, bytes.Length)
   34:  
   35:     ' переводим ответ в XDocument для использования с LINQ
   36:     Dim resp As HttpWebResponse = CType(request.GetResponse(), HttpWebResponse)
   37:     Dim xmlReader As XmlReader = XmlReader.Create(resp.GetResponseStream())
   38:     Dim xdoc As XDocument = XDocument.Load(xmlReader)
   39:     xmlReader.Close()
   40:     Return xdoc
   41: End Function

 

Оба этих метода используют для аутентификации в Twitter с целью последующего чтения или записи объект NetworkCredentials. Методы Twitter API возвращают XML-объекты. GetTwitter и PostTwitter переводят эти XML-документы в объекты XDocument, которые в дальнейшем можно запрашивать посредством LINQ to XML.

Нам надо вызывать два get-метода Twitter: user_timeline и verify_credentials. Имеются два переопределенных метода, которые обращаются к API-вызовам user_ timeline:

C#

    1: public IList<Status> GetUserTimeline(int page, int count)
    2: {
    3:     XDocument xdoc = GetTwitter(string.Format("https://twitter.com/statuses/user_timeline.xml?count={0}&page={1}&id={2}", count, page, Username));
    4:     IList<Status> statuses = ParseStatuses(xdoc);
    5:     return statuses;
    6: }
    7:  
    8: public IList<Status> GetUserTimeline(int since_id, int max_id, int page, int count)
    9: {
   10:     XDocument xdoc = GetTwitter(string.Format("https://twitter.com/statuses/user_timeline.xml?since_id={0}&max_id={1}&count={2}&page={3}&id={4}", since_id, max_id, count, page, Username));
   11:     IList<Status> statuses = ParseStatuses(xdoc);
   12:     return statuses;
   13: }

 

VB

    1: Public Function GetUserTimeline(ByVal page As Integer, ByVal count As Integer) As IList(Of Status)
    2:     Dim xdoc As XDocument = GetTwitter(String.Format("https://twitter.com/statuses/user_timeline.xml?count={0}&page={1}&id={2}", count, page, Username))
    3:     Dim statuses As IList(Of Status) = ParseStatuses(xdoc)
    4:     Return statuses
    5: End Function
    6:  
    7: Public Function GetUserTimeline(ByVal since_id As Integer, ByVal max_id As Integer, ByVal page As Integer, ByVal count As Integer) As IList(Of Status)
    8:     Dim xdoc As XDocument = GetTwitter(String.Format("https://twitter.com/statuses/user_timeline.xml?since_id={0}&max_id={1}&count={2}&page={3}&id={4}", since_id, max_id, count, page, Username))
    9:     Dim statuses As IList(Of Status) = ParseStatuses(xdoc)
   10:     Return statuses
   11: End Function

 

В каждом из этих методов создается URL с соответствующими аргументами строки запроса (их полный список см. в документации Twitter API), а затем вызывается наш метод ParseStatuses с возвращенными XDocument, который даст нам список объектов, описывающих записи. Возвращаемая Twitter запись содержит различные данные, например следующие:

    1: <status>
    2: <created_at>Mon Mar 30 07:20:57 +0000 2009</created_at>
    3: <id>1234567123</id>
    4: <text>Status text</text>
    5: <source>web</source>
    6: <truncated>false</truncated>
    7: <in_reply_to_status_id/>
    8: <in_reply_to_user_id/>
    9: <favorited>false</favorited>
   10:  
   11: <user>
   12: <id>12345678</id>
   13: <name>Some Person</name>
   14: <screen_name>myscreenname</screen_name>
   15: <description/>
   16: <location/>
   17:  
   18: <profile_image_url>
   19: https://static.twitter.com/images/default_profile_normal.png
   20: </profile_image_url>
   21: <url/>
   22: <protected>false</protected>
   23: <followers_count>1</followers_count>
   24: </user>
   25: </status>

 

Нас интересуют лишь некоторые из них, и именно их мы будет анализировать. Это id, text, user (который сам является XML-записью) и created_at.

C#

    1: private IList<Status> ParseStatuses(XContainer container)
    2: {
    3:     // возврат списка объектов Status
    4:     var query = from status in container.Descendants("statuses").Descendants("status")
    5:                 select ParseStatus(status);
    6:     return query.ToList();
    7: }
    8:  
    9: private Status ParseStatus(XDocument xdoc)
   10: {
   11:     // создать из возвращенного XML объект Status
   12:     var query = from status in xdoc.Descendants("status")
   13:                 select ParseStatus(status);
   14:  
   15:     return query.SingleOrDefault();
   16: }
   17:  
   18: private Status ParseStatus(XElement xelement)
   19: {
   20:     Status s = new Status()
   21:                 {
   22:                     ID = (int)xelement.Element("id"),
   23:                     Text = (string)xelement.Element("text"),
   24:                     UserInformation = ParseUserInformation(xelement.Element("user")),
   25:                     // дата в формате для HTTP
   26:                     CreatedAt = DateTime.ParseExact(xelement.Element("created_at").Value,
   27:                             "ddd MMM dd HH:mm:ss zzzz yyyy",
   28:                             CultureInfo.GetCultureInfoByIetfLanguageTag("en-us"),
   29:                             DateTimeStyles.AllowWhiteSpaces)
   30:                 };
   31:  
   32:     return s;
   33: }

 

VB

    1: Private Function ParseStatuses(ByVal container As XContainer) As IList(Of Status)
    2:     ' возврат списка объектов Status
    3:     Dim query = _
    4:         From status In container.Descendants("statuses").Descendants("status") _
    5:         Select ParseStatus(status)
    6:     Return query.ToList()
    7: End Function
    8:  
    9: Private Function ParseStatus(ByVal xdoc As XDocument) As Status
   10:     ' создать из возвращенного XML объект Status
   11:     Dim query = _
   12:         From status In xdoc.Descendants("status") _
   13:         Select ParseStatus(status)
   14:  
   15:     Return query.SingleOrDefault()
   16: End Function
   17:  
   18: Private Function ParseStatus(ByVal xelement As XElement) As Status
   19:     Dim s As New Status() With { _
   20:         .ID = CInt(xelement.Element("id")), _
   21:         .Text = CStr(xelement.Element("text")), _
   22:         .UserInformation = ParseUserInformation(xelement.Element("user")), _
   23:         .CreatedAt = DateTime.ParseExact(xelement.Element("created_at").Value, _
   24:                     "ddd MMM dd HH:mm:ss zzzz yyyy", CultureInfo.GetCultureInfoByIetfLanguageTag("en-us"), DateTimeStyles.AllowWhiteSpaces) }
   25:     Return s
   26: End Function

В этих методах для анализа отдельных объектов сообщений в XML-документе и возврата их в виде списка применяется LINQ to XML. Обратите внимание, что элемент user содержит XML-фрагмент user_information, и метод ParseUserInformation анализирует эти данные:
C#

    1: private UserInformation ParseUserInformation(XContainer container)
    2: {
    3:     // разобрать и вернуть объект UserInformation
    4:     return new UserInformation
    5:     {
    6:         ID = (int)container.Element("id"),
    7:         Name = (string)container.Element("name"),
    8:         ScreenName = (string)container.Element("screen_name")
    9:     };
   10: }

 

VB

    1: Private Function ParseUserInformation(ByVal container As XContainer) As UserInformation
    2:     ' разобрать и вернуть объект UserInformation
    3:     Dim ui As New UserInformation() With { _
    4:         .ID = CInt(container.Element("id")), _
    5:         .Name = CStr(container.Element("name")), _
    6:         .ScreenName = CStr(container.Element("screen_name")) }
    7:  
    8:     Return ui
    9: End Function

 

Вызов API verify_credentials вызывается аналогично, но в этом случае мы ожидаем исключения 401 (Unauthorized) и возвращаем true или false соответственно:

C#

    1: public bool VerifyTwitterCredentials(string username, string password)
    2: {
    3:     try
    4:     {
    5:         WebClient wc = new WebClient();
    6:         wc.Credentials = new NetworkCredential(username, password);
    7:         Stream s = wc.OpenRead("https://twitter.com/account/verify_credentials.xml");
    8:         s.Close();
    9:     }
   10:     catch(WebException we)
   11:     {
   12:         if((we.Response as HttpWebResponse).StatusCode == HttpStatusCode.Unauthorized)
   13:             return false;
   14:         throw;
   15:     }
   16:  
   17:     return true;
   18: }

 

Разобравшись с методами “get”, приступим к созданию методов “post”. Здесь нам также нужны два метода: update и destroy. Первый используется для написания нового сообщения в Twitter, а второй — для удаления существующей записи.

C#

    1: public Status UpdateStatus(string status)
    2: {
    3:     XDocument doc = PostTwitter("https://twitter.com/statuses/update.xml", "status=" + status);
    4:     Status s =  ParseStatus(doc);
    5:  
    6:     // если отправленный текст не получен, мы превысили лимит...
    7:     if(s.Text != HttpUtility.UrlDecode(status))
    8:         throw new TwitterRateLimitException("Twitter upload limit reached.");
    9:     return s;
   10: }
   11:  
   12: public Status Destroy(int status)
   13: {
   14:     XDocument doc = PostTwitter("https://twitter.com/statuses/destroy/" + status + ".xml", "id=" + status);
   15:     return ParseStatus(doc);
   16: }

 

VB

    1: Public Function VerifyTwitterCredentials(ByVal username As String, ByVal password As String) As Boolean
    2:     Try
    3:         Dim wc As New WebClient()
    4:         wc.Credentials = New NetworkCredential(username, password)
    5:         Dim s As Stream = wc.OpenRead("https://twitter.com/account/verify_credentials.xml")
    6:         s.Close()
    7:     Catch we As WebException
    8:         If (TryCast(we.Response, HttpWebResponse)).StatusCode = HttpStatusCode.Unauthorized Then
    9:             Return False
   10:         End If
   11:         Throw
   12:     End Try
   13:  
   14:     Return True
   15: End Function
   16:  

 

В методе UpdateStatus контролируется непревышение ограничения Twitter на число отправляемых сообщений. Twitter имеет ограничение в 100 сообщений в час. Единственная возможность определить, что этот предел достигнут (которую я нашел после долгих поисков), это сравнение текста из возвращенного элемента status с отправленным текстом. Если достигнут предел, будет возвращен последний допустимый элемент status и, следовательно, текстовые блоки не будут совпадать. В таком случае мы вызываем собственное пользовательское исключение TwitterRateLimitException, которое обрабатывается классами пользовательского интерфейса.

TwitterDrive

Научившись разговаривать с Twitter, надо написать программу, которая с помощью наших методов будет сохранять и извлекать данные файлов. Это реализуется в классе TwitterDrive.

Одна из моих целей при написании этого приложения — обеспечить многопоточность, чтобы интерфейс пользователя оставался реактивным при отправке и загрузке данных. Соответственно, в этом классе устанавливаются три события, которые могут из пользовательского интерфейса использоваться для получения периодической информации о состоянии:

C#

    1: public class ChunkEventArgs : EventArgs
    2: {
    3:     public Status Status;
    4:     public int ChunkLength;
    5:     public int Total;
    6:  
    7:     public ChunkEventArgs(Status status, int length, int total)
    8:     {
    9:         Status = status;
   10:         ChunkLength = length;
   11:         Total = total;
   12:     }
   13: }
   14:  
   15: ...
   16:  
   17: // обработчики событий для асинхронной передачи файла
   18: public event EventHandler<ChunkEventArgs> ChunkUpload;
   19: public event EventHandler<ChunkEventArgs> ChunkDownload;
   20: public event EventHandler<EventArgs> TransferComplete;

 

VB

    1: Public Class ChunkEventArgs
    2:     Inherits EventArgs
    3:     Public Status As Status
    4:     Public ChunkLength As Integer
    5:     Public Total As Integer
    6:  
    7:     Public Sub New(ByVal status As Status, ByVal length As Integer, ByVal total As Integer)
    8:         Status = status
    9:         ChunkLength = length
   10:         Me.Total = total
   11:     End Sub
   12: End Class
   13:  

 

Эти обработчики будут вызываться в нужные моменты и предоставлять сведения, необходимые для отображения в пользовательском интерфейсе индикатора выполнения и других данных о состоянии процесса.

Метод UploadFile передает указанный файл. Содержимое файла загружается в память, сжимается, кодируется в base64, а в завершение производится URL-кодирование. Затем полученная большая строка разбивается на фрагменты по 140 символов, которые по одному отправляются в Twitter. После загрузки каждого очередного фрагмента вызывается событие ChunkUpload. После отправки всех фрагментов создается новый объект FileEntry, который добавляется к индексу файла в памяти, а этот индекс записывается в верхушку списка состояния Twitter.

C#

    1: public void UploadFile(string path)
    2: {
    3:     Status s = null;
    4:     int startID = 0;
    5:     int length = 0;
    6:     int chunkLength = 140;
    7:  
    8:     // кодировать файл
    9:     string file = EncodeFile(path);
   10:  
   11:     // отправить фрагменты
   12:     for(int i = 0; i < file.Length; i+= chunkLength)
   13:     {
   14:         // вычислить правильную длину (не указывать 140 для последнего неполного фрагмента)
   15:         string chunk = file.Substring(i, Math.Min(chunkLength, file.Length-i));
   16:  
   17:         // обработка случая, когда фрагмент заканчивается в середине кодовой 
   18:         // последовательности; в этом случае убрать лишний символ и сбросить счетчик
   19:         if(chunk.EndsWith("%2"))
   20:         {
   21:             chunk = chunk.Substring(0, chunk.Length-2);
   22:             i -= 2;
   23:         }
   24:  
   25:         if(chunk.EndsWith("%"))
   26:         {
   27:             chunk = chunk.Substring(0, chunk.Length-1);
   28:             i -= 1;
   29:         }
   30:  
   31:         try
   32:         {
   33:             // отправить фрагменты
   34:             s = _twitter.UpdateStatus(chunk);
   35:  
   36:             // уведомить слушателей об окончании передачи
   37:             if(ChunkUpload != null)
   38:                 ChunkUpload(this, new ChunkEventArgs(s, chunk.Length, file.Length));
   39:         }
   40:         catch(TwitterRateLimitException)
   41:         {
   42:             throw new TwitterDriveException("Twitter upload limit reached.  Please try again later.");
   43:         }
   44:  
   45:         if(i == 0)
   46:             startID = s.ID;
   47:  
   48:         length++;
   49:     }
   50:  
   51:     // создать для данного файла FileEntry
   52:     FileEntry fe = new FileEntry()
   53:     {
   54:         Filename = Path.GetFileName(path),
   55:         StartStatus = startID,
   56:         EndStatus = s.ID,
   57:         Length = length,
   58:         FileIndex = GetNextIndex()
   59:     };
   60:  
   61:     // обновить индекс
   62:     UpdateFileIndex(fe);
   63:  
   64:     // уведомить о завершении
   65:     if(TransferComplete != null)
   66:         TransferComplete(this, null);
   67: }

 

VB

    1: Public Function UploadFile(ByVal filepath As String)
    2:     Dim s As Status = Nothing
    3:     Dim startID As Integer = 0
    4:     Dim length As Integer = 0
    5:     Dim chunkLength As Integer = 140
    6:  
    7:     ' кодировать файл
    8:     Dim file As String = EncodeFile(filepath)
    9:  
   10:     ' отправить фрагменты
   11:     For i As Integer = 0 To file.Length - 1 Step chunkLength
   12:         ' вычислить правильную длину (не указывать 140 для последнего неполного фрагмента)
   13:         Dim chunk As String = file.Substring(i, Math.Min(chunkLength, file.Length-i))
   14:  
   15:         ' обработка случая, когда фрагмент заканчивается в середине кодовой 
   16:         ' последовательности; в этом случае убрать лишний символ и сбросить счетчик
   17:         If chunk.EndsWith("%2") Then
   18:             chunk = chunk.Substring(0, chunk.Length-2)
   19:             i -= 2
   20:         End If
   21:  
   22:         If chunk.EndsWith("%") Then
   23:             chunk = chunk.Substring(0, chunk.Length-1)
   24:             i -= 1
   25:         End If
   26:  
   27:         Try
   28:             ' отправить фрагменты
   29:             s = _twitter.UpdateStatus(chunk)
   30:  
   31:             ' уведомить слушателей об окончании передачи
   32:             RaiseEvent ChunkUpload(Me, New ChunkEventArgs(s, chunk.Length, file.Length))
   33:         Catch e1 As TwitterRateLimitException
   34:             Throw New TwitterDriveException("Twitter upload limit reached.  Please try again later.")
   35:         End Try
   36:  
   37:         If i = 0 Then
   38:             startID = s.ID
   39:         End If
   40:  
   41:         length += 1
   42:     Next i
   43:  
   44:     ' создать для данного файла FileEntry
   45:     Dim fe As New FileEntry() With {.Filename = Path.GetFileName(filepath), .StartStatus = startID, .EndStatus = s.ID, .Length = length, .FileIndex = GetNextIndex()}
   46:  
   47:     ' обновить индекс
   48:     UpdateFileIndex(fe)
   49:  
   50:     ' уведомить о завершении
   51:     RaiseEvent TransferComplete(Me, Nothing)
   52: End Function

Рассмотрим подробней метод EncodeFile:
C#

    1: private string EncodeFile(string path)
    2: {
    3:     // загрузить файл
    4:     byte[] buff = File.ReadAllBytes(path);
    5:  
    6:     // сжать
    7:     MemoryStream ms = new MemoryStream();
    8:     GZipStream gs = new GZipStream(ms, CompressionMode.Compress);
    9:     gs.Write(buff, 0, buff.Length);
   10:     gs.Close();
   11:  
   12:     byte[] buffCompressed = ms.ToArray();
   13:  
   14:     // base64, urlencode
   15:     return HttpUtility.UrlEncode(Convert.ToBase64String(buffCompressed));
   16: }

 

VB

    1: Private Function EncodeFile(ByVal path As String) As String
    2:     ' загрузить файл
    3:     Dim buff() As Byte = File.ReadAllBytes(path)
    4:  
    5:     ' сжать
    6:     Dim ms As New MemoryStream()
    7:     Dim gs As New GZipStream(ms, CompressionMode.Compress)
    8:     gs.Write(buff, 0, buff.Length)
    9:     gs.Close()
   10:  
   11:     Dim buffCompressed() As Byte = ms.ToArray()
   12:  
   13:     ' base64, urlencode
   14:     Return HttpUtility.UrlEncode(Convert.ToBase64String(buffCompressed))
   15: End Function

 

Программа помещает считываемые байты в массив. Затем создается объект MemoryStream, который связывается с объектом GZipStream (сжатие). Массив байт записывается в поток, сжимающий данные «на лету». После закрытия этого потока, вызов метода ToArray класса MemoryStream возвращает байтовый массив со сжатыми данными. Этот массив кодируется в base64 (превращается в текст) и в завершение производится URL-кодирование для передачи в Twitter.

После передачи каждого файла записывается его индекс. Он состоит из строк с ограничителями, содержащими данные из объекта FileEntry. Каждый файл загружается как последовательность сообщений с концевым маркером, указывающим конец индексного файла.

C#

    1: private void WriteFileEntries()
    2: {
    3:     // записать концевой маркер (первым, поскольку в дальнейшем будет обратный порядок)
    4:     _twitter.UpdateStatus(FileEntryEnd);
    5:  
    6:     for(int i = 0; i < _fileEntries.Count; i++)
    7:         WriteFileEntry(_fileEntries[i]);
    8: }
    9:  
   10: private void WriteFileEntry(FileEntry fe)
   11: {
   12:     // простой список с ограничителями
   13:     string entry = string.Format(FileEntryHeader +
   14:                                 "{0}" + FileEntrySeparator + 
   15:                                 "{1}" + FileEntrySeparator +
   16:                                 "{2}" + FileEntrySeparator +
   17:                                 "{3}" + FileEntrySeparator +
   18:                                 "{4}", 
   19:                                 fe.Filename, fe.StartStatus, fe.EndStatus, fe.Length, fe.FileIndex);
   20:     _twitter.UpdateStatus(entry);
   21: }

 

VB

    1: Private Sub WriteFileEntries()
    2:     ' записать концевой маркер (первым, поскольку в дальнейшем будет обратный порядок)
    3:     _twitter.UpdateStatus(FileEntryEnd)
    4:  
    5:     Dim i As Integer = 0
    6:     Do While i < _fileEntries.Count
    7:         WriteFileEntry(_fileEntries(i))
    8:         i += 1
    9:     Loop
   10: End Sub
   11:  
   12: Private Sub WriteFileEntry(ByVal fe As FileEntry)
   13:     ' простой список с ограничителями
   14:     Dim entry As String = String.Format(FileEntryHeader & "{0}" & FileEntrySeparator & "{1}" & FileEntrySeparator & "{2}" & FileEntrySeparator & "{3}" & FileEntrySeparator & "{4}", fe.Filename, fe.StartStatus, fe.EndStatus, fe.Length, fe.FileIndex)
   15:     _twitter.UpdateStatus(entry)
   16: End Sub
   17:  

 

Список файла может быть получен и разобран следующим кодом:

C#

    1: public IList<FileEntry> GetFileIndex()
    2: {
    3:     bool end = false;
    4:     int page = 1;
    5:  
    6:     // последним должен быть индекс, но при сбое передачи это не так
    7:     while(!end && page < 5)
    8:     {
    9:         IList<Status> indexes = _twitter.GetUserTimeline(page, 200);
   10:  
   11:         _fileEntries.Clear();
   12:  
   13:         // разобрать записи файла
   14:         foreach(Status index in indexes)
   15:         {
   16:             FileEntry fe = ParseFileIndexString(index.Text);
   17:             if(fe != null)
   18:             {
   19:                 fe.IndexStatusId = index.ID;
   20:                 _fileEntries.Add(fe);
   21:             }
   22:  
   23:             if(index.Text.StartsWith(FileEntryEnd))
   24:             {
   25:                 end = true;
   26:                 break;
   27:             }
   28:         }
   29:         page++;
   30:     }
   31:     return _fileEntries;
   32: }
   33:  
   34: private FileEntry ParseFileIndexString(string index)
   35: {
   36:     if(!index.StartsWith(FileEntryHeader))
   37:         return null;
   38:  
   39:     string[] fileEntry = index.Split(Convert.ToChar(FileEntrySeparator));
   40:     FileEntry fe = new FileEntry()
   41:                     {
   42:                         Filename = fileEntry[0].Replace(FileEntryHeader, string.Empty),
   43:                         StartStatus = int.Parse(fileEntry[1]),
   44:                         EndStatus = int.Parse(fileEntry[2]),
   45:                         Length = int.Parse(fileEntry[3]),
   46:                         FileIndex = int.Parse(fileEntry[4])
   47:                     };
   48:     return fe;
   49: }

 

VB

    1: Public Function GetFileIndex() As IList(Of FileEntry)
    2:     Dim [end] As Boolean = False
    3:     Dim page As Integer = 1
    4:  
    5:     ' последним должен быть индекс, но при сбое передачи это не так
    6:     Do While (Not [end]) AndAlso page < 5
    7:         Dim indexes As IList(Of Status) = _twitter.GetUserTimeline(page, 200)
    8:  
    9:         _fileEntries.Clear()
   10:  
   11:         ' разобрать записи файла
   12:         For Each index As Status In indexes
   13:             Dim fe As FileEntry = ParseFileIndexString(index.Text)
   14:             If fe IsNot Nothing Then
   15:                 fe.IndexStatusId = index.ID
   16:                 _fileEntries.Add(fe)
   17:             End If
   18:  
   19:             If index.Text.StartsWith(FileEntryEnd) Then
   20:                 [end] = True
   21:                 Exit For
   22:             End If
   23:         Next index
   24:         page += 1
   25:     Loop
   26:     Return _fileEntries
   27: End Function
   28:  
   29: Private Function ParseFileIndexString(ByVal index As String) As FileEntry
   30:     If (Not index.StartsWith(FileEntryHeader)) Then
   31:         Return Nothing
   32:     End If
   33:  
   34:     Dim fileEntry() As String = index.Split(Convert.ToChar(FileEntrySeparator))
   35:     Dim fe As New FileEntry() With {.Filename = fileEntry(0).Replace(FileEntryHeader, String.Empty), .StartStatus = Integer.Parse(fileEntry(1)), .EndStatus = Integer.Parse(fileEntry(2)), .Length = Integer.Parse(fileEntry(3)), .FileIndex = Integer.Parse(fileEntry(4))}
   36:     Return fe
   37: End Function

 

Приведенный ниже код извлекает временную шкалу пользователя, проходит по сообщениям и находит записи индекса файла (которые начинаются с соответствующих ограничителей). Когда индекс найден, его данные помещаются в объект FileEntry в памяти. Как видите, метод GetFileIndex считывает до 1000 сообщений в поисках концевого маркера.

Теперь, когда мы можем передавать файлы и строить их индексы, нам нужна возможность загрузки, раскодирования и сохранения файлов. Это делается методом DownloadFile:

C#

    1: public void DownloadFile(FileEntry fe, string path)
    2: {
    3:     StringBuilder sb = new StringBuilder(fe.Length * 140);
    4:  
    5:     // получить состояния
    6:     IList<Status> chunks = _twitter.GetUserTimeline(fe.StartStatus-1, fe.EndStatus, 1, 200);
    7:  
    8:     // упорядочить их от старых к новым
    9:     var orderedChunks = from chunk in chunks
   10:                         orderby chunk.ID ascending
   11:                         select chunk;
   12:  
   13:     foreach(Status chunk in orderedChunks)
   14:     {
   15:         // отсечь лишние символы
   16:         string newChunk = chunk.Text.TrimEnd('.').Trim();
   17:         sb.Append(newChunk);
   18:  
   19:         // уведомить слушателей, что мы загрузили фрагмент
   20:         if(ChunkDownload != null)
   21:             ChunkDownload(this, new ChunkEventArgs(chunk, newChunk.Length, fe.Length * 140));
   22:     }
   23:  
   24:     // раскодировать и записать файл
   25:     byte[] buff = DecodeFile(sb.ToString());
   26:     File.WriteAllBytes(Path.Combine(path, fe.Filename), buff);
   27:  
   28:     // уведомить о завершении
   29:     if(TransferComplete != null)
   30:         TransferComplete(this, null);
   31: }

VB

    1: Public Function DownloadFile(ByVal fe As FileEntry, ByVal filepath As String)
    2:     Dim sb As New StringBuilder(fe.Length * 140)
    3:  
    4:     ' получить состояния
    5:     Dim chunks As IList(Of Status) = _twitter.GetUserTimeline(fe.StartStatus-1, fe.EndStatus, 1, 200)
    6:  
    7:     ' упорядочить их от старых к новым
    8:     Dim orderedChunks = _
    9:         From chunk In chunks _
   10:         Order By chunk.ID Ascending _
   11:         Select chunk
   12:  
   13:     For Each chunk As Status In orderedChunks
   14:         ' отсечь лишние символы
   15:         Dim newChunk As String = chunk.Text.TrimEnd("."c).Trim()
   16:         sb.Append(newChunk)
   17:  
   18:         ' уведомить слушателей, что мы загрузили фрагмент
   19:         RaiseEvent ChunkDownload(Me, New ChunkEventArgs(chunk, newChunk.Length, fe.Length * 140))
   20:     Next chunk
   21:  
   22:     ' раскодировать и записать файл
   23:     Dim buff() As Byte = DecodeFile(sb.ToString())
   24:     File.WriteAllBytes(Path.Combine(filepath, fe.Filename), buff)
   25:  
   26:     ' уведомить о завершении
   27:     RaiseEvent TransferComplete(Me, Nothing)
   28: End Function

 

Данный метод возвращает временную шкалу пользователя с начальной и конечной записями, указанными в объекте FileEntry. Список состояний отсортирован по возрастанию (т. е. от старых записей к новым). Каждое сообщение добавляется к предыдущему с использованием объекта StringBuilder. После обработки всех фрагментов файл раскодируется и сохраняется в выбранном месте.

C#

    1: public byte[] DecodeFile(string data)
    2: {
    3:     // преобразовать base64 в двоичный код
    4:     byte[] buff = Convert.FromBase64String(data);
    5:  
    6:     // разжать
    7:     MemoryStream ms = new MemoryStream(buff);
    8:     GZipStream gs = new GZipStream(ms, CompressionMode.Decompress, false);
    9:  
   10:     // исходный
   11:     byte[] decompressed = ReadAllBytes(gs);
   12:     return decompressed;
   13: }

 

VB

    1: Public Function DecodeFile(ByVal data As String) As Byte()
    2:     ' преобразовать base64 в двоичный код
    3:     Dim buff() As Byte = Convert.FromBase64String(data)
    4:  
    5:     ' разжать
    6:     Dim ms As New MemoryStream(buff)
    7:     Dim gs As New GZipStream(ms, CompressionMode.Decompress, False)
    8:  
    9:     ' исходный
   10:     Dim decompressed() As Byte = ReadAllBytes(gs)
   11:     Return decompressed
   12: End Function

 

Для раскодирования файла выполняются действия, обратные ранее проделанным: файл преобразуется из кодировки base64 в двоичный, данные разжимаются с помощью GZipStream и в завершение полученное содержимое возвращается вызывающей стороне для сохранения на диске.

Здесь описаны основные моменты, полная картина у вас сложится, если вы посмотрите файлы TwitterDrive.cs/vb .

Интерфейс пользователя

Последний момент — интерфейс пользователя.

4-1-2009 4-01-42 AM 

Это очень простой интерфейс для передачи, загрузки и удаления файлов. При запуске приложения подключаются три события TwitterDrive: ChunkUpload, ChunkDownload и TransferComplete. Эти обработчики событий используются для обновления индикатора выполнения в диалоговом окне, всплывающем при передаче файла.

Когда выбрано действие (передача или загрузка) и файл, создается новый поток, в котором запускается реальный процесс. Вот как выглядит процесс загрузки:

C#

    1: // породить новый поток для получения файла
    2: Thread t = new Thread(() => _twitterDrive.DownloadFile(fe, fbd.SelectedPath));
    3: t.Start();
    4:  
    5: if(_progress.ShowDialog() == DialogResult.Cancel)
    6:     t.Abort();
    7: else
    8:     MessageBox.Show("File download complete.", "TwitterDrive", MessageBoxButtons.OK, MessageBoxIcon.Information);

VB

    1: ' породить новый поток для получения файла
    2: Dim t As New Thread(CType(Function() _twitterDrive.DownloadFile(fe, fbd.SelectedPath), ThreadStart))
    3: t.Start()
    4:  
    5: If _progress.ShowDialog() = System.Windows.Forms.DialogResult.Cancel Then
    6:     t.Abort()
    7: Else
    8:     MessageBox.Show("File download complete.", "TwitterDrive", MessageBoxButtons.OK, MessageBoxIcon.Information)
    9: End If

 

Создается новый поток, параметром ThreadStart которого является результат выполнения метода DownloadFile класса TwitterDrive. Поток запускается и отображается диалоговое окно с индикатором хода выполнения. Если это окно закрыть, поток прервется; само окно закроется, когда возникнет событие TransferComplete класса TwitterDrive.

Работа с приложением
  1. Для работы с TwitterDrive создайте новую учетную запись в Twitter.
  2. Введите идентификационные данные для новой записи и щелкните Refresh. Если вы хотите только загружать чьи-то файлы, введите только имя пользователя и щелкните Refresh.
  3. Передавайте или загружайте файлы.
Завершение

Вот мы и закончили. Имеем работающее приложение с бесполезными функциями. Попробуйте найти его ограничения. Только не используйте свою реальную учетную запись в Twitter, а то получите кучу злых последователей.

Благодарности

Особые благодарности Марку Заугу (Mark Zaugg), Дэну Фернандесу (Dan Fernandez) и Джованни Монтроне (Giovanni Montrone) за помощь в тестировании моего приложения. Джованни, кроме того, можно назвать соавтором самой идеи, которая родилась из шуточной просьбы опубликовать файл в Twitter. Так что это он во всем виноват.

Также благодарю Клинта Раткэса (Clint Rutkas) за изготовление значка (это комбинация значков SkyDrive и Twitter).

Об авторе

Брайан имеет звание Microsoft C# MVP (EN). Он активно программирует для .NET начиная с ранних бета-версий этой платформы, вышедшей в 2000 г., а прочие технологии Майкрософт начал использовать еще раньше. Кроме .NET Брайан отлично разбирается в C, C++ и языке ассемблера для различных процессоров. Он также является специалистом по таким технологиям, как веб-разработка, графическое представление документов, ГИС, графика, разработка игр и программирование устройств. Брайан имеет опыт разработки приложений для здравоохранения, а также в создании решений для портативных устройств. Кроме того, Брайан является соавтором книги «Coding4Fun: 10 .NET Programming Projects for Wiimote, YouTube, World of Warcraft, and More (EN)», вышедшей в издательстве O'Reilly (EN). Ранее вышла книга «Debugging ASP.NET» (EN) издательства New Riders, соавтором которой он также является. Брайан также один из авторов сайта MSDN Coding4Fun (EN).