12 Replies Latest reply on Jan 10, 2009 3:57 AM by (Herb_Albert)

    Using the LrHttp

      Hi all,
      I hope you can help.

      I'm writing a plugin to allow users to export straight to a webservice written in asp.net.

      here are the params needed.
      http://barnaland.is/dev/DataStorage.asmx?op=AddFile

      int
            int
            int
            int
            int
            string
            base64Binary
            string
            string
            string

      I've played with the sample plugins in the sdk be able to get usertoken etc.. so I seem to be able to cover all but the acutal upload.

      now how do I change this so the webservice gets all the params needed?

      local postUrl = ''
      info( 'uploading photo', params.filePath )
      local filePath = assert( params.filePath )
      params.filePath = nil
      local fileName = LrPathUtils.leafName( filePath )
      params.auth_token = params.auth_token or prefs.token
      params.api_sig = FlickrAPI.makeApiSignature( params )

      local mimeChunks = {}

      for argName, argValue in pairs( params ) do
         if argName ~= 'api_sig' then
           mimeChunks[ #mimeChunks + 1 ] = { name = argName, value = argValue }
         end
      end

      mimeChunks[ #mimeChunks + 1 ] = { name = 'api_sig', value = params.api_sig }
      mimeChunks[ #mimeChunks + 1 ] = { name = 'photo', fileName = fileName, filePath = filePath, contentType = 'application/octet-stream' }
           
      -- Post it and wait for confirmation.
           
      local result = LrHttp.postMultipart( postUrl, mimeChunks )

      I hope you can help.
        • 1. Re: Using the LrHttp
          Level 1
          Hi Stefan,

          unfortunately your request is rather unspecific. Maybe you could make it more concrete. Is it about the example code (I'd say drop it and assemble the method call yourself), API usage or how to match your webservice's interface?

          With the WS specification provided, we've got the names to use and some hints about how their values should look like. You are basically doing a HTTP POST with the code shown above. Although in "MIME Multipart" form instead of the x-www-form-urlencoded type POST shown in the webservice's example.

          So my concern would be whether the WS accepts the multipart message with the file content being sent as binary data. I am not enough into these things to predict whether it can be expected to do so.

          Herb
          • 2. Re: Using the LrHttp
            Level 1
            Hi Herb,

            Thank you very much for your response.

            I would like to know how to match the webservice interface.

            Are the pairs in the mimechunks table the right place for the params or should I include them in my query string?

            If I add them in the mimeChunk is this the right way

            local mimeChunks = {}
            mimeChunks[ 1 ] = {
            folderId=1, counter=1, fileLength=1,height=1, name='bla', file=filePath , userToken=9999, apiKey=0, contentType = 'application/octet-stream' }

            local result = LrHttp.postMultipart( postUrl, mimeChunks )

            and finally do you think that matter the difference between mime multipart and x-www-form-urlencoded and is this contenttype right fyrir this kind of data.

            Sorry for these questions... but Im new to LUA.. :)
            • 3. Re: Using the LrHttp
              Level 1
              If a multipart post is okay, then the pairs should be put inside the 'content' table of tables (mimeChunks). As we are doing a http post (not http get), the query (url) should only contain the location, no data.

              The structure of the content/mimeChunks is as follows:

              local content = {
              
                {
                  name = 'folderId',
                  value = '1'
                },
                {
                  name = 'name',
                  value = 'My First Uploaded Picture',
                },
                -- ... and so on ...
                {
                  name = 'file',
                  filePath = 'c:/temp/myImage.png',
                  -- no value here as the value is taken from the filePath file
                  contentType = 'application/octet-stream'
                }
              }


              The structure may be constructed programmatically, I guess the flickr example code wasn't too bad.

              About multipart or not -- not sure. The other approach would be to use the LrHttp.post() method and construct the message and message body yourself to resemble the 'http post' shown in the WS examples. For the body, this leads to basically one long string with concatenated name and value pairs. I guess (although not sure) the file content is expected to be encoded in base64 (like in the SOAP message format also described), as the WSDL says "array of strings" type, and base64 splits data up into lines. There is base64 functionality provided by LR's API (in LrStringUtils, if I remember correctly).

              Sorry that I cannot give definitive answers, just guesses... I'd enjoy to open an account on the website and test it myself -- unfortunately my Islandic is nonexistent. I am not even sure if it is an open user group and what kind of website it is...

              Keep us informed
              Herb
              • 4. Re: Using the LrHttp
                Level 1
                Hi,, thanks alot

                Im sorry to say that the webservice does not seem to allow multipart, so I had to go with post

                As I am constructing my postContenct I try to convert the photo to a base64 by doing this.

                LrStringUtils.encodeBase64(filePath) where filepath is a path like 'c:/temp/myImage.png'

                But the encode only seems to encode the actual string but not the photo itself, causing error in the webservice.... So How can I convert this photo :)

                btw.
                I see Frontur aso runs an english version of the web, and there it's called http://babyworld.net/

                Its free to register and get an API key... so please... if you have the time.. try for your self. :)
                • 5. Re: Using the LrHttp
                  Level 1
                  Hi.

                  ................ !!! Just closed my browser window with an almost complete, in-depth answer. :-( So this is the short version:

                  - Thanks for the link to the english site. Great.
                  - Tried hard to post x-www-form-urlencoded messages. urlencode and base64 back and forth, (almost) no success.
                  - multipart/form-data is being rejected (as you said).

                  So I decided to assemble SOAP messages (using LrXml) which finally work. I was able to put images to the web. Message format is like shown in the example, with base64 encoded file data inserted.

                  Referring your previous message, you need to encode the file's content, not it's path:

                  local imgFilePath = 'c:/temp/image.jpg'
                  
                  local file = io.open(imgFilePath, 'rb') -- binary read mode
                  local data = file:read("*all")
                  file:close()
                  local base64data = LrStringUtils.encodeBase64(data)


                  I also learnt that the userToken has to be requested from the webservice as well, as it is not the username or password. I'll see if I can clean up my code later (or within the next days) and post some.

                  Herb
                  • 6. Re: Using the LrHttp
                    Level 1
                    Great,
                    If you can post some more code how to do this, that would really make my day.

                    Thank you very much

                    :)
                    • 7. Re: Using the LrHttp
                      Level 1
                      Let's see if I am allowed to post the complete example. 8-) <br /> <br /> <pre> --[[ <br /> This is an attempt to use the http://babyworld.net/dev/ webservice <br /> from within Adobe Photoshop Lightroom 2. <br /> <br /> 2009-01-08 <br />--]] <br /> <br />local LrHttp = import 'LrHttp' <br />local LrTasks = import 'LrTasks' <br />local LrStringUtils = import 'LrStringUtils' <br />local LrXml = import 'LrXml' <br />local LrLogger = import 'LrLogger' <br /> <br />local myLogger = LrLogger('libraryLogger') <br />myLogger:enable('logfile') <br /> <br />--[[ <br />WS implements communication with the webservice <br /> <br />Methods exposed to the user: <br />- WS:LoginApiKey() <br />- WS:AddFile(...) <br />- ... and all other operations still need to be implemented ... <br /> <br />Attributes exposed to the user: <br />- WS.userToken [need to manually request this from the ws] <br />- WS.apiKey [you get this from the website] <br />- WS.lastReplyMessage (readonly) [for debugging whenever an operation failed] <br /> <br />NOTICE: <br />Testing code only with almost no error handling <br />--]] <br /> <br />local WS = { <br /> xmlns = 'http://frontur.com/api', <br /> -- some presets ; change these <br /> userToken = 'uP2V...', <br /> apiKey = 'bc9...', <br /> lastReplyMessage = nil, -- message sent by the WS stored here for debugging <br />} <br /> <br />-- get a single string from a result xml document <br />WS.extractTagValue = function(self, xmlString, tagname) <br /> <br /> searchTag = function(dom, tagname) <br />  if dom:name() == tagname then <br />   return dom:childAtIndex(1):serialize() <br />  else <br />   for i=1,(dom:childCount()) do <br />    local result = searchTag(dom:childAtIndex(i), tagname) <br />    if result ~= nil then return result end <br />   end <br />   return nil <br />  end <br /> end <br /> <br /> local dom = LrXml.parseXml(xmlString) <br /> return searchTag(dom, tagname) <br /> <br />end <br /> <br />-- compile the xml document that is going to be sent to the webservice <br />WS.assembleSoapBody = function (self, operation, params) <br /> <br /> local soapEnvelopeAttributes = {} <br /> soapEnvelopeAttributes["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance" <br /> soapEnvelopeAttributes["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema" <br /> soapEnvelopeAttributes["xmlns:soap"] = "http://schemas.xmlsoap.org/soap/envelope/" <br /> <br /> local xmlDeclaration = '<?xml version="1.0" encoding="utf-8"?> ' <br /> <br /> local xml = LrXml.createXmlBuilder(true) -- omit declaration <br /> xml:beginBlock("soap:Envelope", soapEnvelopeAttributes) <br /> xml:beginBlock("soap:Body") <br /> xml:beginBlock(operation, { xmlns = self.xmlns }) <br /> <br /> for paramName, paramValue in pairs(params) do <br />  xml:tag(paramName, paramValue) <br /> end <br /> <br /> xml:endBlock(operation) <br /> xml:endBlock("soap:Body") <br /> xml:endBlock("soap:Envelope") <br /> <br /> return xmlDeclaration .. '\n' .. xml:serialize() <br />end <br /> <br />-- send message to the webservice <br />-- notice that it does not create an asynchronous task, <br />-- so the user of the code has to do this to fulfil LR's requirements <br />WS.post = function(self, url, soapAction, body) <br /> <br /> local headers = { <br />  { <br />   field = 'Content-Type', <br />   value = 'text/xml; charset=utf-8' <br />  }, <br />  { <br />   field = 'SOAPAction', <br />   value = soapAction <br />  } <br /> } <br /> <br /> local resultString, resultTable = LrHttp.post(url, body, headers) <br /> <br /> self.lastReplyMessage = resultString <br /> return resultString <br /> <br />end <br /> <br />-- "public" function for corresponding ws operation <br /> <br />WS.AddFile = function(self, folderId, filepath, width, height, name, text, counter) <br /> <br /> local url = 'http://babyworld.net/dev/DataStorage.asmx' <br /> local soapAction = 'http://frontur.com/api/AddFile' <br /> local operation = 'AddFile' <br /> <br /> -- FIXME: check if file exists etc... <br /> local file = io.open(filepath, 'rb') <br /> local data = file:read("*all") <br /> file:close() <br /> local base64data = LrStringUtils.encodeBase64(data) <br /> <br /> local params = { <br />  folderId = folderId, <br />  file = base64data, <br />  width = width, <br />  height = height, <br />  name = name, <br />  text = text, <br />  counter = counter, <br />  userToken = self.userToken, <br />  apiKey = self.apiKey, <br /> } <br /> <br /> local soapBody = self:assembleSoapBody(operation, params) <br /> local result = self:post(url, soapAction, soapBody) <br /> <br /> return tonumber(self:extractTagValue(result, 'AddFileResult')) <br />end <br /> <br />-- "public" function for corresponding ws operation <br /> <br />WS.LoginApiKey = function(self) <br /> <br /> local url = 'http://babyworld.net/dev/Authentication.asmx' <br /> local soapAction = 'http://frontur.com/api/LoginApiKey' <br /> local operation = 'LoginApiKey' <br /> <br /> local params = { <br />  apiKey = self.apiKey, <br /> } <br /> <br /> local soapBody = self:assembleSoapBody(operation, params) <br /> local result = self:post(url, soapAction, soapBody) <br /> <br /> local returnValue = false <br /> if self:extractTagValue(result, 'LoginApiKeyResult') == 'true' then <br />  returnValue = true <br /> end <br /> <br /> return returnValue <br /> <br />end <br /> <br />-- end of WS <br /> <br />-- Example usage. <br />-- We start a new asynchronous task as LrHttp.posts need to be inside one. <br /> <br />local task = function() <br /> <br /> if WS:LoginApiKey() then <br /> <br />  myLogger:info('Login successful for api key ' .. WS.apiKey) <br /> <br />  local folderId = 54321 -- there is another ws operation to get this ;) <br />  local uploadedPhotoId = WS:AddFile(folderId, 'c:/temp/image.jpg', 0, 0, 'a name', 'a description', 0) <br /> <br />  if uploadedPhotoId &gt; 0 then <br />   myLogger:info('Photo uploaded. ID: ' .. uploadedPhotoId) <br />  else <br />   myLogger:error('Photo upload failed') <br />   myLogger:debug(WS.lastReplyMessage) <br />  end <br /> <br /> else <br />  myLogger:error('Login unsuccessful') <br />  myLogger:debug(WS.lastReplyMessage) <br /> end <br /> <br /> <br />end <br />LrTasks.startAsyncTask(task) </pre>
                      • 8. Re: Using the LrHttp
                        Level 1
                        Hi..

                        This is perfect.... Thank you soooo much.. I'll try this soon and let you know. :)
                        • 9. Re: Using the LrHttp
                          Level 1
                          Thanks Herb.. this works perfectly :)

                          My wife will be very happy with this feature.. :)
                          • 10. Re: Using the LrHttp
                            Level 1
                            Thanks, nice. :-) Are you going to extend the set of operations for real world usage?

                            BTW,
                            I noticed an error in the example code, at the bottom, when a photo is ultimately uploaded:
                            local uploadedPhotoId = WS:AddFile(...)
                            
                            if uploadedPhotoId > 0 then ...
                            This expects the method to return an int value. In reality, it can as well be nil, when there's something wrong on communication level or with the input. Since it's an error to compare nil to an int, the code above should rather look like that:
                            if uploadedPhotoId and uploadedPhotoId > 0 then ...
                            
                            • 11. Re: Using the LrHttp
                              Level 1
                              Yes,
                              I plan to do so. I have been implementing it and now I can upload as many photos as I wish in one export :)

                              Another thing..

                              Do you know what control ( in lrview ) can I use to select multiple chooses.... I'm running into a small problem when I publish a new album, I must choose in witch web I want it to be visible in.. you see,, one account can hold multiple "webs",,, say... as a parent has 3 children, they can create 3 separate webs but,, let some album be visible on all webs or whatever they want.
                              So I was thinking about using the pop-up_menu control,, but I don't konw How ( or if ) multible select is posible... or can I use the combo_box.

                              I also tried to create 7 checkboxes like this

                              f:checkbox {
                              title = bind 'te_web6',,
                              enabled = bind 'web6',
                              value = true,
                              },

                              but sadly that does not work either because the title seems to be static... :(
                              • 12. Re: Using the LrHttp
                                Level 1
                                About multiple selection... I did not find a control in LR that provides multiple selection.

                                The checkbox approach should work, keep in mind that the bind_to_object must be a property table (-> LrBinding.makePropertyTable() ). So that value changes are propagated to the controls.

                                Drawback is that the checkboxes would be kind of hard coded to a certain maximum amount of webs (set up 2, 3, 10, 100 checkboxes?). To keep things plain and simple, I'd probably prefer to just provide one popup menu to list all available webs accompanied with ADD, REMOVE and REMOVE_ALL buttons - and the actual webs selection displayed as a plain concatenated string - or something like that.

                                From a technical aspect, the popup menu approach seems cleaner to me. Although the user might feel more comfortable to simply check some webs.