private TaggedFileEntry ParseXmlAtomEntry(XmlNode node, XmlNamespaceManager mgr) { if (mgr == null) mgr = new XmlNamespaceManager(node.OwnerDocument.NameTable); if (!mgr.HasNamespace("live")) mgr.AddNamespace("live", "http://api.live.com/schemas"); if (!mgr.HasNamespace("atom")) mgr.AddNamespace("atom", "http://www.w3.org/2005/Atom"); XmlNode title = node.SelectSingleNode("atom:title", mgr); XmlNode id = node.SelectSingleNode("live:resourceId", mgr); XmlNode url = node.SelectSingleNode("atom:id", mgr); if (title == null || id == null || url == null) return null; XmlNode updated = node.SelectSingleNode("atom:updated", mgr); XmlNode size = node.SelectSingleNode("live:size", mgr); XmlNode type = node.SelectSingleNode("live:type", mgr); string u = url.InnerText; XmlNode content = node.SelectSingleNode("atom:content", mgr); if (content != null) content = content.Attributes["src"]; if (content != null) u = content.Value; string editLink = null; string altLink = null; foreach (XmlNode n in node.SelectNodes("atom:link", mgr)) if (n.Attributes["rel"] != null && n.Attributes["href"] != null) { if (n.Attributes["rel"].Value == "edit-media") editLink = n.Attributes["href"].Value; else if (n.Attributes["rel"].Value == "alternate") altLink = n.Attributes["href"].Value; } TaggedFileEntry tf = new TaggedFileEntry(title.InnerText, u, id.InnerText, altLink, editLink); try { tf.LastAccess = tf.LastModification = DateTime.Parse(updated.InnerText); } catch { } try { tf.Size = long.Parse(size.InnerText); } catch { } try { tf.IsFolder = type.InnerText.Equals("Library", StringComparison.InvariantCultureIgnoreCase) || type.InnerText.Equals("Folder", StringComparison.InvariantCultureIgnoreCase); } catch { } return tf; }
public void Put(string remotename, System.IO.Stream stream) { try { Google.Documents.Document folder = GetFolder(); //Special, since uploads can overwrite or create, // we must figure out if the file exists in advance. //Unfortunately it would be wastefull to request the list // for each upload request, so we rely on the cache being // correct TaggedFileEntry doc = null; if (m_files == null) doc = TryGetFile(remotename); else m_files.TryGetValue(remotename, out doc); try { string resumableUri; if (doc != null) { if (doc.MediaUrl == null) { //Strange, we could not get the edit url, perhaps it is readonly? //Fallback strategy is "delete-then-upload" try { this.Delete(remotename); } catch { } doc = TryGetFile(remotename); if (doc != null || doc.MediaUrl == null) throw new Exception(string.Format(Strings.GoogleDocs.FileIsReadOnlyError, remotename)); } } //File does not exist, we upload a new one if (doc == null) { //First we need to get a resumeable upload url HttpWebRequest req = (HttpWebRequest)WebRequest.Create("https://docs.google.com/feeds/upload/create-session/default/private/full/" + System.Web.HttpUtility.UrlEncode(folder.ResourceId) + "/contents?convert=false"); req.Method = "POST"; req.Headers.Add("X-Upload-Content-Length", stream.Length.ToString()); req.Headers.Add("X-Upload-Content-Type", "application/octet-stream"); req.UserAgent = USER_AGENT; req.Headers.Add("GData-Version", "3.0"); //Build the atom entry describing the file we want to create string labels = ""; foreach (string s in m_labels) if (s.Trim().Length > 0) labels += string.Format(ATTRIBUTE_TEMPLATE, s); //Apply the name and content-type to the not-yet-uploaded file byte[] data = System.Text.Encoding.UTF8.GetBytes(string.Format(CREATE_ITEM_TEMPLATE, System.Web.HttpUtility.HtmlEncode(remotename), labels)); req.ContentLength = data.Length; req.ContentType = "application/atom+xml"; //Authenticate our request m_cla.ApplyAuthenticationToRequest(req); using (System.IO.Stream s = req.GetRequestStream()) s.Write(data, 0, data.Length); using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse()) { int code = (int)resp.StatusCode; if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); resumableUri = resp.Headers["Location"]; } } else { //First we need to get a resumeable upload url HttpWebRequest req = (HttpWebRequest)WebRequest.Create(doc.MediaUrl); req.Method = "PUT"; req.Headers.Add("X-Upload-Content-Length", stream.Length.ToString()); req.Headers.Add("X-Upload-Content-Type", "application/octet-stream"); req.UserAgent = USER_AGENT; req.Headers.Add("If-Match", doc.ETag); req.Headers.Add("GData-Version", "3.0"); //This is a blank marker request req.ContentLength = 0; //Bad... docs say "text/plain" or "text/xml", but really needs to be content type, otherwise overwrite fails //req.ContentType = "text/plain"; req.ContentType = "application/octet-stream"; //Authenticate our request m_cla.ApplyAuthenticationToRequest(req); using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse()) { int code = (int)resp.StatusCode; if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); resumableUri = resp.Headers["Location"]; } } //Ensure that we have a resumeable upload url if (resumableUri == null) throw new Exception(Strings.GoogleDocs.NoResumeURLError); string id = null; byte[] buffer = new byte[8 * 1024]; int retries = 0; long initialPosition; DateTime initialRequestTime = DateTime.Now; while (stream.Position != stream.Length) { initialPosition = stream.Position; long postbytes = Math.Min(stream.Length - initialPosition, TRANSFER_CHUNK_SIZE); //Post a fragment of the file as a partial request HttpWebRequest req = (HttpWebRequest)WebRequest.Create(resumableUri); req.Method = "PUT"; req.UserAgent = USER_AGENT; req.ContentLength = postbytes; req.ContentType = "application/octet-stream"; req.Headers.Add("Content-Range", string.Format("bytes {0}-{1}/{2}", initialPosition, initialPosition + (postbytes - 1), stream.Length.ToString())); //Copy the current fragment of bytes using (System.IO.Stream s = req.GetRequestStream()) { long bytesleft = postbytes; long written = 0; int a; while (bytesleft != 0 && ((a = stream.Read(buffer, 0, (int)Math.Min(buffer.Length, bytesleft))) != 0)) { s.Write(buffer, 0, a); bytesleft -= a; written += a; } s.Flush(); if (bytesleft != 0 || postbytes != written) throw new System.IO.EndOfStreamException(); } try { using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse()) { int code = (int)resp.StatusCode; if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); //If all goes well, we should now get an atom entry describing the new element System.Xml.XmlDocument xml = new XmlDocument(); using (System.IO.Stream s = resp.GetResponseStream()) xml.Load(s); System.Xml.XmlNamespaceManager mgr = new XmlNamespaceManager(xml.NameTable); mgr.AddNamespace("atom", "http://www.w3.org/2005/Atom"); mgr.AddNamespace("gd", "http://schemas.google.com/g/2005"); id = xml.SelectSingleNode("atom:entry/atom:id", mgr).InnerText; string resourceId = xml.SelectSingleNode("atom:entry/gd:resourceId", mgr).InnerText; string url = xml.SelectSingleNode("atom:entry/atom:content", mgr).Attributes["src"].Value; string mediaUrl = null; foreach(XmlNode n in xml.SelectNodes("atom:entry/atom:link", mgr)) if (n.Attributes["rel"] != null && n.Attributes["href"] != null &&n.Attributes["rel"].Value.EndsWith("#resumable-edit-media")) { mediaUrl = n.Attributes["href"].Value; break; } if (doc == null) { TaggedFileEntry tf = new TaggedFileEntry(remotename, stream.Length, initialRequestTime, initialRequestTime, resourceId, url, mediaUrl, resp.Headers["ETag"]); m_files.Add(remotename, tf); } else { //Since we update an existing item, we just need to update the ETag doc.ETag = resp.Headers["ETag"]; } } retries = 0; } catch (WebException wex) { bool acceptedError = wex.Status == WebExceptionStatus.ProtocolError && wex.Response is HttpWebResponse && (int)((HttpWebResponse)wex.Response).StatusCode == 308; //Mono does not give us the response object, // so we rely on the error code being present // in the string, not ideal, but I have found // no other workaround :( if (XervBackup.Library.Utility.Utility.IsMono) { acceptedError |= wex.Status == WebExceptionStatus.ProtocolError && wex.Message.Contains("308"); } //Accept the 308 until we are complete if (acceptedError && initialPosition + postbytes != stream.Length) { retries = 0; //Accept the 308 until we are complete } else { //Retries are handled in XervBackup, but it is much more efficient here, // because we only re-submit the last TRANSFER_CHUNK_SIZE bytes, // instead of the entire file retries++; if (retries > 2) throw; else System.Threading.Thread.Sleep(2000 * retries); stream.Position = initialPosition; } } } if (string.IsNullOrEmpty(id)) throw new Exception(Strings.GoogleDocs.NoIDReturnedError); } catch { //Clear the cache as we have no idea what happened m_files = null; throw; } } catch (Google.GData.Client.CaptchaRequiredException cex) { throw new Exception(string.Format(Strings.GoogleDocs.CaptchaRequiredError, CAPTCHA_UNLOCK_URL), cex); } }