TwitterDrive – революционное онлайн-хранилище
Опубликовано 1 апреля 2009 в 12:05 | Coding4Fun
В этой статье Брайан Пик (Brian Peek) описывает TwitterDrive, приложение, использующее службу сообщений Twitter для хранения файлов. |
|
Брайан Пик (Brian Peek (EN)) ASPSOFT, Inc. (EN) Сложность: средняя. Необходимое время: 2-3 часа. Цена : бесплатно. Оборудование: нет. Загрузки: приложение, исходный текст. Дискуссионный форум : здесь (EN). |
Введение
Как вы уже, наверное, догадались, TwitterDrive — это мой первоапрельский подарок Coding4Fun в этом году. Хотя это приложение действительно работает, ограничения, накладываемые Twitter, лишают его практической ценности. Однако польза от него все же есть: в данном описании рассказывается об использовании Twitter API, LINQ to XML, потоков и множества других вещей.
Как это работает
Не очень-то хорошо, на самом деле. Идея следующая: берем файл, сжимаем его, кодируем в uuencode/base64 (представляя двоичные данные в виде текста), а затем отправляем на Twitter в виде последовательности 140-символьных сообщений. После завершения загрузки файла записываем его индекс, который потребуется для последующего извлечения это файла.
Давайте начнем с 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 .
Интерфейс пользователя
Последний момент — интерфейс пользователя.
Это очень простой интерфейс для передачи, загрузки и удаления файлов. При запуске приложения подключаются три события 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.
Работа с приложением
- Для работы с TwitterDrive создайте новую учетную запись в Twitter.
- Введите идентификационные данные для новой записи и щелкните Refresh. Если вы хотите только загружать чьи-то файлы, введите только имя пользователя и щелкните Refresh.
- Передавайте или загружайте файлы.
Завершение
Вот мы и закончили. Имеем работающее приложение с бесполезными функциями. Попробуйте найти его ограничения. Только не используйте свою реальную учетную запись в 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).
Comments
- Anonymous
May 07, 2009
Опубликовано 1 апреля 2009 в 12:10 | Coding4Fun Вы еще не знаете? TwitterDrive ( EN ) — это революционная