Compare commits
10 Commits
c4e08faa16
...
8597fd1b49
Author | SHA1 | Date |
---|---|---|
The Magician | 8597fd1b49 | |
The Magician | 83db84091f | |
The Magician | 05211354ea | |
The Magician | 853d2ecf69 | |
The Magician | 07cd251aaa | |
The Magician | dfa7bddf96 | |
The Magician | eac6deee1d | |
The Magician | 68d37afec7 | |
The Magician | a60ca8eb52 | |
The Magician | fd77e2075a |
2
Makefile
2
Makefile
|
@ -1,4 +1,2 @@
|
||||||
run:
|
|
||||||
./jardin.py
|
|
||||||
test:
|
test:
|
||||||
find -name "*jardin.py" | entr -cd ./test_jardin.py
|
find -name "*jardin.py" | entr -cd ./test_jardin.py
|
||||||
|
|
128
jardin.py
128
jardin.py
|
@ -1,8 +1,15 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import bs4
|
||||||
|
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
|
||||||
def initialize_webdriver():
|
def initialize_webdriver():
|
||||||
return webdriver.Firefox()
|
return webdriver.Firefox()
|
||||||
|
@ -29,15 +36,118 @@ def login_with_password(browser, email, password):
|
||||||
click_password_button(browser)
|
click_password_button(browser)
|
||||||
input_password(browser, password)
|
input_password(browser, password)
|
||||||
|
|
||||||
def main():
|
def click_go_to_your_garden(browser):
|
||||||
with initialize_webdriver() as browser:
|
browser.implicitly_wait(100)
|
||||||
|
goToYourGardenButton = browser.find_element(By.LINK_TEXT, "GO TO YOUR GARDEN")
|
||||||
|
goToYourGardenButton.click()
|
||||||
|
|
||||||
|
def get_garden_filenames(browser):
|
||||||
|
titles = []
|
||||||
|
soup = bs4.BeautifulSoup(browser.page_source, "lxml")
|
||||||
|
|
||||||
|
for item in soup.find_all(attrs={"class": "name"}):
|
||||||
|
titles.append(item.text)
|
||||||
|
|
||||||
|
return titles
|
||||||
|
|
||||||
|
def get_local_filenames(directory):
|
||||||
|
return os.listdir(directory)
|
||||||
|
|
||||||
|
def get_missing_filenames(localFiles, gardenFiles):
|
||||||
|
missingFiles = []
|
||||||
|
|
||||||
|
for localFile in localFiles:
|
||||||
|
if localFile not in gardenFiles:
|
||||||
|
missingFiles.append(localFile)
|
||||||
|
|
||||||
|
return missingFiles
|
||||||
|
|
||||||
|
def create_file_input(browser, userId):
|
||||||
|
javascript = """const fileInput = document.createElement("input");
|
||||||
|
fileInput.id = "upload";
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.multiple = true;
|
||||||
|
|
||||||
|
fileInput.addEventListener("change", () => {
|
||||||
|
for (const file of fileInput.files) {
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
xhr.responseType = "text";
|
||||||
|
|
||||||
|
xhr.open("POST", "/users/""" + userId + """/pipe", true);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
parent: null,
|
||||||
|
name: file.name
|
||||||
|
};
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
||||||
|
xhr.setRequestHeader("X-Data", encodeURI(JSON.stringify(data)));
|
||||||
|
|
||||||
|
xhr.send(file);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Miro.request(
|
||||||
|
"POST",
|
||||||
|
"/users/""" + userId + """/pipe",
|
||||||
|
{"Content-Type": "application/octet-stream"},
|
||||||
|
file,
|
||||||
|
function(xhr) {
|
||||||
|
console.log("Started uploading " + file.name + "...");
|
||||||
|
|
||||||
|
xhr.addEventListener("readystatechange", function() {
|
||||||
|
if (xhr.readyState == XMLHttpRequest.OPENED) {
|
||||||
|
const data = {
|
||||||
|
parent: null,
|
||||||
|
name: file.name
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
||||||
|
xhr.setRequestHeader("X-Data", encodeURI(JSON.stringify(data)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true)
|
||||||
|
.then(Miro.response(
|
||||||
|
function(xhr) {
|
||||||
|
console.log("Uploaded " + file.name);
|
||||||
|
}),
|
||||||
|
function(xhr, error) {
|
||||||
|
console.log("Failed to upload " + file.name + " due to error:");
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
fileInput.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
"""
|
||||||
|
|
||||||
|
browser.execute_script(javascript)
|
||||||
|
|
||||||
|
def get_user_id(browser):
|
||||||
|
url = browser.current_url
|
||||||
|
userId = re.search("[0-9a-f]{24}", url)[0]
|
||||||
|
return userId
|
||||||
|
|
||||||
|
def upload_files(browser, directory, missingFiles):
|
||||||
|
userId = get_user_id(browser)
|
||||||
|
create_file_input(browser, userId)
|
||||||
|
uploadInput = browser.find_element(By.ID, "upload")
|
||||||
|
for file in missingFiles:
|
||||||
|
uploadInput.send_keys(directory + file)
|
||||||
|
|
||||||
|
def main(email, password, directory):
|
||||||
|
browser = initialize_webdriver()
|
||||||
navigate_to_filegarden(browser)
|
navigate_to_filegarden(browser)
|
||||||
# Go To Your Garden
|
login_with_password(browser, email, password)
|
||||||
# Log in (email)
|
click_go_to_your_garden(browser)
|
||||||
# Go To Your Garden
|
garden_filenames = get_garden_filenames(browser)
|
||||||
# Get list of filenames in File Garden
|
local_filenames = get_local_filenames(directory)
|
||||||
# Get list of files in target upload directory that don't exist in File Garden (test based on filename? size? file contents?)
|
missing_files = get_missing_filenames(local_filenames, garden_filenames)
|
||||||
# For each file in the second list, go through the file upload process
|
upload_files(browser, directory, missing_files)
|
||||||
|
#browser.close()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main(sys.argv[1], sys.argv[2], sys.argv[3])
|
||||||
|
|
155
test_jardin.py
155
test_jardin.py
|
@ -66,15 +66,162 @@ class TestJardin(unittest.TestCase):
|
||||||
mockClickPasswordButton.assert_called_once_with(mockFirefox)
|
mockClickPasswordButton.assert_called_once_with(mockFirefox)
|
||||||
mockInputPassword.assert_called_once_with(mockFirefox, mockPassword)
|
mockInputPassword.assert_called_once_with(mockFirefox, mockPassword)
|
||||||
|
|
||||||
|
@patch("jardin.webdriver.Firefox")
|
||||||
|
def test_click_go_to_your_garden_clicks_button(self, mockFirefox):
|
||||||
|
jardin.click_go_to_your_garden(mockFirefox)
|
||||||
|
|
||||||
|
mockFirefox.find_element.assert_called_with(By.LINK_TEXT, "GO TO YOUR GARDEN")
|
||||||
|
mockFirefox.find_element.return_value.click.assert_called_once()
|
||||||
|
|
||||||
|
@patch("jardin.webdriver.Firefox")
|
||||||
|
@patch("jardin.bs4.BeautifulSoup")
|
||||||
|
def test_get_garden_filenames_creates_beautifulsoup_and_finds_names(self, mockBeautifulSoup, mockFirefox):
|
||||||
|
jardin.get_garden_filenames(mockFirefox)
|
||||||
|
|
||||||
|
mockBeautifulSoup.assert_called_once_with(mockFirefox.page_source, "lxml")
|
||||||
|
mockBeautifulSoup.return_value.find_all.assert_called_once_with(attrs={"class": "name"})
|
||||||
|
|
||||||
|
@patch("jardin.webdriver.Firefox")
|
||||||
|
def test_get_garden_filenames_returns_filenames_correctly(self, mockFirefox):
|
||||||
|
mockFirefox.page_source = """
|
||||||
|
<html lang="en">
|
||||||
|
<body class="mdc-typography">
|
||||||
|
<div id="items" class="items">
|
||||||
|
<a class="item typeFile selected" draggable="false" ondragstart="return false;" href="https://file.garden/fakeid/file1.jpg">
|
||||||
|
<div class="cell icon">
|
||||||
|
<button class="mdc-icon-button material-icons" type="button">image</button>
|
||||||
|
</div>
|
||||||
|
<div class="cell name" title="file1.jpg">file1.jpg</div>
|
||||||
|
<div class="cell size" title="10 B">10 B</div>
|
||||||
|
<div class="cell type" title="image/jpeg">image/jpeg</div>
|
||||||
|
<div class="cell date" title="Wed Aug 30 2023 13:04:10 GMT+0100 (British Summer Time)">Aug 30 2023 13:04:10</div>
|
||||||
|
</a>
|
||||||
|
<a class="item typeFile" draggable="false" ondragstart="return false;" href="https://file.garden/fakeid/file2.jpg">
|
||||||
|
<div class="cell icon">
|
||||||
|
<button class="mdc-icon-button material-icons" type="button">image</button>
|
||||||
|
</div>
|
||||||
|
<div class="cell name" title="file2.jpg">file2.jpg</div>
|
||||||
|
<div class="cell size" title="20 B">20 B</div>
|
||||||
|
<div class="cell type" title="image/jpeg">image/jpeg</div>
|
||||||
|
<div class="cell date" title="Wed Jun 29 2022 17:37:14 GMT+0100 (British Summer Time)">Jun 29 2022 17:37:14</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
filenames = jardin.get_garden_filenames(mockFirefox)
|
||||||
|
|
||||||
|
self.assertEqual(filenames, ["file1.jpg", "file2.jpg"])
|
||||||
|
|
||||||
|
@patch("jardin.os.listdir")
|
||||||
|
def test_get_local_filenames_gets_filenames_in_local_directory(self, mockListdir):
|
||||||
|
mockDirectory = "/home/luser/gardenfiles/"
|
||||||
|
mockFiles = ["file1", "file2", "file3"]
|
||||||
|
mockListdir.return_value = mockFiles
|
||||||
|
|
||||||
|
localFiles = jardin.get_local_filenames(mockDirectory)
|
||||||
|
|
||||||
|
mockListdir.assert_called_once_with(mockDirectory)
|
||||||
|
self.assertEqual(localFiles, mockFiles)
|
||||||
|
|
||||||
|
def test_get_missing_filenames_returns_local_filenames_without_garden_filenames(self):
|
||||||
|
mockLocalFilenames = ["file1.jpg", "file2.jpg", "file3.jpg", "file4.jpg", "file5.jpg"]
|
||||||
|
mockGardenFilenames = ["file2.jpg", "file4.jpg", "file6.jpg", "file8.jpg", "file10.jpg"]
|
||||||
|
|
||||||
|
missingFilenames = jardin.get_missing_filenames(mockLocalFilenames, mockGardenFilenames)
|
||||||
|
|
||||||
|
self.assertEqual(missingFilenames, ["file1.jpg", "file3.jpg", "file5.jpg"])
|
||||||
|
|
||||||
|
@patch("jardin.webdriver.Firefox")
|
||||||
|
def test_create_file_input_runs_correct_script(self, mockFirefox):
|
||||||
|
expectedScript = """const fileInput = document.createElement("input");
|
||||||
|
fileInput.id = "upload";
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.multiple = true;
|
||||||
|
|
||||||
|
fileInput.addEventListener("change", () => {
|
||||||
|
for (const file of fileInput.files) {
|
||||||
|
console.log(file); // Debug
|
||||||
|
// Miro.request(file); // What I'll probably actually use (or wrapper method)
|
||||||
|
//addFile(file); // Original (token not accessible from minified code)
|
||||||
|
}
|
||||||
|
fileInput.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
"""
|
||||||
|
|
||||||
|
jardin.create_file_input(mockFirefox)
|
||||||
|
|
||||||
|
mockFirefox.execute_script.assert_called_once_with(expectedScript)
|
||||||
|
|
||||||
|
@patch("jardin.webdriver.Firefox")
|
||||||
|
def test_get_user_id_returns_user_id_from_url(self, mockFirefox):
|
||||||
|
mockUserId = "73927475b259be67f8cea98f"
|
||||||
|
mockUrl = f"https://filegarden.com/users/{mockUserId}/garden/#"
|
||||||
|
mockFirefox.current_url = mockUrl
|
||||||
|
|
||||||
|
userId = jardin.get_user_id(mockFirefox)
|
||||||
|
|
||||||
|
self.assertEqual(userId, mockUserId)
|
||||||
|
|
||||||
|
@patch("jardin.get_user_id")
|
||||||
|
@patch("jardin.create_file_input")
|
||||||
|
@patch("jardin.webdriver.Firefox")
|
||||||
|
def test_upload_files_calls_correct_setup_methods(self, mockFirefox, mockCreateFileInput, mockGetUserId):
|
||||||
|
jardin.upload_files(mockFirefox, "", [])
|
||||||
|
|
||||||
|
mockCreateFileInput.assert_called_once_with(mockFirefox)
|
||||||
|
mockGetUserId.assert_called_once_with(mockFirefox)
|
||||||
|
|
||||||
|
@patch("jardin.get_user_id")
|
||||||
|
@patch("jardin.webdriver.Firefox")
|
||||||
|
def test_upload_files_calls_upload_file_once_per_file(self, mockFirefox, mockGetUserId):
|
||||||
|
mockGetUserId.return_value = "userid"
|
||||||
|
mockFiles = ["file1.jpg", "file2.jpg", "file3.jpg"]
|
||||||
|
mockDirectory = "/home/luser/gardenfiles/"
|
||||||
|
|
||||||
|
jardin.upload_files(mockFirefox, mockDirectory, mockFiles)
|
||||||
|
|
||||||
|
mockFirefox.find_element.return_value.send_keys.assert_has_calls([
|
||||||
|
call(mockDirectory + "file1.jpg"),
|
||||||
|
call(mockDirectory + "file2.jpg"),
|
||||||
|
call(mockDirectory + "file3.jpg")
|
||||||
|
])
|
||||||
|
|
||||||
@patch("jardin.initialize_webdriver")
|
@patch("jardin.initialize_webdriver")
|
||||||
@patch("jardin.navigate_to_filegarden")
|
@patch("jardin.navigate_to_filegarden")
|
||||||
|
@patch("jardin.login_with_password")
|
||||||
|
@patch("jardin.click_go_to_your_garden")
|
||||||
|
@patch("jardin.get_garden_filenames")
|
||||||
|
@patch("jardin.get_local_filenames")
|
||||||
|
@patch("jardin.get_missing_filenames")
|
||||||
|
@patch("jardin.upload_files")
|
||||||
def test_main_calls_methods_in_correct_order(self,
|
def test_main_calls_methods_in_correct_order(self,
|
||||||
mockInitializeWebdriver,
|
mockUploadFiles,
|
||||||
mockNavigateToFilegarden):
|
mockGetMissingFilenames,
|
||||||
jardin.main()
|
mockGetLocalFilenames,
|
||||||
|
mockGetGardenFilenames,
|
||||||
|
mockClickGoToYourGarden,
|
||||||
|
mockLoginWithPassword,
|
||||||
|
mockNavigateToFilegarden,
|
||||||
|
mockInitializeWebdriver):
|
||||||
|
mockEmail = "email@mail.com"
|
||||||
|
mockPassword = "p4$$w0rd"
|
||||||
|
mockDirectory = "/home/luser/gardenfiles/"
|
||||||
|
|
||||||
|
jardin.main(mockEmail, mockPassword, mockDirectory)
|
||||||
|
|
||||||
mockInitializeWebdriver.assert_called_once()
|
mockInitializeWebdriver.assert_called_once()
|
||||||
mockNavigateToFilegarden.assert_called_once()
|
mockNavigateToFilegarden.assert_called_once_with(mockInitializeWebdriver.return_value)
|
||||||
|
mockLoginWithPassword.assert_called_once_with(mockInitializeWebdriver.return_value, mockEmail, mockPassword)
|
||||||
|
mockClickGoToYourGarden.assert_called_once_with(mockInitializeWebdriver.return_value)
|
||||||
|
mockGetGardenFilenames.assert_called_once_with(mockInitializeWebdriver.return_value)
|
||||||
|
mockGetLocalFilenames.assert_called_once_with(mockDirectory)
|
||||||
|
mockGetMissingFilenames.assert_called_once_with(mockGetLocalFilenames.return_value, mockGetGardenFilenames.return_value)
|
||||||
|
mockUploadFiles.assert_called_once_with(mockInitializeWebdriver.return_value, mockDirectory, mockGetMissingFilenames.return_value)
|
||||||
|
#mockInitializeWebdriver.return_value.close.assert_called_once()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Loading…
Reference in New Issue