[SOLVED] Syncing portraits, two sites with restapi

How can I sync/add user portraits between two plone sites via restapi?

If I have

 portrait = portal_membership.getPersonalPortrait(userid) 

I can create a user on Plone site B (from Plone site A) with

        payload = {
            "email": email,
            "fullname": fullname,
            "sendPasswordReset": True
        }
        
        
        
        response = requests.post(users_endpoint, auth=auth, headers=headers, json=payload)
  1. Can I 'add the portrait' the same way or do I need to first create the user and then use another endpoint to update it

  2. Do I need to convert the image (data) to base64 ?

  3. Or is it better to make 'my own endpoint' and call it with the url of the existing image?


For alternative 3, I assume I can set it (on site B) (with try:)

 resp = requests.get(portrait_url)  # portrait url should be /portal_memberdata/portraits/[user_id]
                if resp.status_code == 200:
                    portrait_img = BytesIO(resp.content)
                    portrait_img.filename = "portrait.jpg" 

 if portrait_img:
            portal_membership.changeMemberPortrait(portrait_img, user_id)

You can pass the portrait as mapping with base64 encoded image on update (PATCH) of a user using the restapi. For some reason it is not possible on create.

Thanks. Works.

I paste the code here for reference. (The only thing 'missing' is keeping the original file-name)

    users_endpoint = f"{project_url}/@users"
    
    headers = {
        "Accept": "application/json",
        "Content-Type": "application/json"
    }

    for userid in user_list:
        user = api.user.get(userid=userid)

        if user is None:
            print(f"⚠️ User '{userid}' not found")
            continue

        # Plone PAS user properties
        username = user.getUserName()
        fullname = user.getProperty("fullname")
        email = user.getProperty("email")

        payload = {
            "email": email,
            "fullname": fullname,
            "username": username, 
            "sendPasswordReset": True,
            # … etc
        }
        
        response = requests.post(users_endpoint, auth=auth, headers=headers, json=payload)
        
        if response.status_code in (200, 201):  # 201 = created
            # print(f" User {email} created")
            
            # Upload portrait if exists
            portal_membership = api.portal.get_tool('portal_membership')
            portrait = portal_membership.getPersonalPortrait(userid) 
            if portrait and hasattr(portrait, 'data'):
                portrait_endpoint = response.json()['@id']
                portrait_bytes = portrait.data or None # the binary image, None if it is the 'default image, then skip'
                
                if portrait_bytes:                        
                    portrait_mime = getattr(portrait, "contentType", "image/jpeg")
                    # filename = portrait.__name__ or "portrait"
                    filename =  "portrait"
                    portrait_b64 = base64.b64encode(portrait_bytes).decode("utf-8")
                    
                    r = requests.patch(portrait_endpoint, 
                                    headers =  headers, 
                                    json={'portrait': {'content-type': portrait_mime , 
                                                        'data': portrait_b64, 
                                                        'encoding': "base64", 
                                                        'filename': filename}}, 
                                    auth=auth)
                    
                    if r.status_code == 204:
                        print(f"✅ Portrait for '{userid}' uploaded successfully")
                    else:
                        print(f"⚠️ Failed to upload portrait for '{userid}'")
        elif response.status_code == 409:
            print(f"⚠️ User {email} already exists")
        else:
            print(f"❌ Error creating {email}: {response.status_code} {response.text}")
            
        # TO do: Add messages, maybe, info / warning