Compare commits
9 Commits
d247ecec54
...
refactor-e
| Author | SHA1 | Date | |
|---|---|---|---|
| 06f81652b7 | |||
| ed6d1dd40d | |||
| 7b1a7ea30d | |||
| 5b0bfb3619 | |||
| 74647bcdfb | |||
| 0ee55ee73d | |||
| 89e8c5d9f7 | |||
| 76b0745cc3 | |||
| bc2d4384c9 |
11
BUGs
11
BUGs
@@ -1,12 +1,5 @@
|
||||
### Next: 146
|
||||
BUG-143: if I skip next fast enough in files_ip, it can get the warning about changes to entry list... (would have though I disabled next page until loaded)
|
||||
BUG-142: after transforming, the face data is still in the old spots, really should delete it / make it recalc
|
||||
BUG-141: can currently try to flip a video (in a highlighted group)
|
||||
BUG-140: When db is restarted underneath PA, it crashes job mgr... It should just accept timeouts, and keep trying to reconnect every 2? mins
|
||||
BUG-125: when an image is highlighted, then post the contextmenu on a different image - the highlight does not move to the new image
|
||||
and the selected menu function processes the original or the new depending on the way the code works.
|
||||
There is a chance we need to change the document on click to a mouse down (or whatever the context menu
|
||||
uses for default), rather than just fix the highlight
|
||||
### Next: 147
|
||||
BUG-146: with an empty DB, I See 'No files in Path!' twice (for file*, except for files_rbp)
|
||||
BUG-118: can move files from Bin path, but it leaves the del_file entry for it - need to remove it
|
||||
BUG-117: when search returns files that can be deleted and/or restored, the icon stays as delete and tries to delete!
|
||||
BUG-106: cant add trudy /pat? as refimgs via FaceDBox
|
||||
|
||||
@@ -682,7 +682,6 @@ function getPage(pageNumber, successCallback, viewingIdx=0)
|
||||
$('#ra').prop('disabled', true)
|
||||
const startIndex = (pageNumber - 1) * OPT.how_many;
|
||||
const endIndex = startIndex + OPT.how_many;
|
||||
console.log('pageNumber is: ' + pageNumber + ', about to make pageList from si=' +startIndex + ', to ei=' + endIndex )
|
||||
pageList = entryList.slice(startIndex, endIndex);
|
||||
|
||||
// set up data to send to server to get the entry data for entries in pageList
|
||||
@@ -760,6 +759,10 @@ function resetNextPrevButtons()
|
||||
// get list of eids for the next page, also make sure next/prev buttons make sense for page we are on
|
||||
function nextPage(successCallback)
|
||||
{
|
||||
// start with disabling more next presses until we are ready to process them
|
||||
$('.prev').prop('disabled', true).addClass('disabled');
|
||||
$('.next').prop('disabled', true).addClass('disabled');
|
||||
|
||||
// pageList[0] is the first entry on this page
|
||||
const currentPage=getPageNumberForId( pageList[0] )
|
||||
// should never happen / just return pageList unchanged
|
||||
@@ -775,6 +778,10 @@ function nextPage(successCallback)
|
||||
// get list of eids for the prev page, also make sure next/prev buttons make sense for page we are on
|
||||
function prevPage(successCallback)
|
||||
{
|
||||
// start with disabling more prev presses until we are ready to process them
|
||||
$('.prev').prop('disabled', true).addClass('disabled');
|
||||
$('.next').prop('disabled', true).addClass('disabled');
|
||||
|
||||
// pageList[0] is the first entry on this page
|
||||
const currentPage=getPageNumberForId( pageList[0] )
|
||||
// should never happen / just return pageList unchanged
|
||||
@@ -917,6 +924,12 @@ $.contextMenu({
|
||||
selector: '.entry',
|
||||
itemClickEvent: "click",
|
||||
build: function($triggerElement, e) {
|
||||
// if we are not in the highlight set, then move the highlight to this element
|
||||
if( ! $(e.currentTarget).is('.highlight') )
|
||||
{
|
||||
$('.highlight').removeClass('highlight');
|
||||
$(e.currentTarget).addClass('highlight')
|
||||
}
|
||||
// when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln
|
||||
if( NoSel() || e.ctrlKey || e.shiftKey )
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// can only have 1 ammendment per image, its grayed out for other changes
|
||||
function removeAmendment( id )
|
||||
{
|
||||
console.log( 'removing amendment for: ' + id )
|
||||
document.amendments=document.amendments.filter(obj => obj.eid !== id)
|
||||
}
|
||||
|
||||
@@ -11,11 +10,16 @@ function removeAmendment( id )
|
||||
function handleTransformImageJobCompleted(job, entry)
|
||||
{
|
||||
removeAmendment( entry.id )
|
||||
// update viewer and files* views, in case we view/go up without a new page load
|
||||
// force reload with timestamped version of im.src
|
||||
im.src=im.src + '?t=' + new Date().getTime();
|
||||
DrawImg()
|
||||
// update viewer if we are viewing an image
|
||||
if( document.viewing )
|
||||
{
|
||||
// force reload with timestamped version of im.src
|
||||
im.src=im.src + '?t=' + new Date().getTime();
|
||||
DrawImg()
|
||||
}
|
||||
|
||||
// ALWAYS update files* div as we could go back to this from a viewer, and
|
||||
// the thumbnail needs the updated data
|
||||
idx = entryList.indexOf(entry.id)
|
||||
// replace data for this entry now its been transformed
|
||||
document.entries[idx]=entry
|
||||
|
||||
11
main.py
11
main.py
@@ -66,12 +66,19 @@ app.config['LDAP_USER_DN'] = 'ou=users'
|
||||
app.config['LDAP_GROUP_DN'] = 'ou=groups'
|
||||
app.config['LDAP_USER_RDN_ATTR'] = 'uid'
|
||||
app.config['LDAP_USER_LOGIN_ATTR'] = 'uid'
|
||||
app.config['LDAP_BIND_USER_DN'] = None
|
||||
app.config['LDAP_BIND_USER_PASSWORD'] = None
|
||||
app.config['LDAP_GROUP_OBJECT_FILTER'] = '(objectclass=posixGroup)'
|
||||
app.config['LDAP_BIND_USER_DN'] = None
|
||||
app.config['LDAP_BIND_USER_PASSWORD'] = None
|
||||
|
||||
# stop db restarts from causing stales and client-side 'server errors' - its a
|
||||
# touch hacky, e.g. it issues a select 1 before EVERY request, likely should
|
||||
# ditch this and just have a short-lived pool, but need to work out if/where I
|
||||
# can catch the right exception myself and then dont need this, but for now...
|
||||
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
||||
"pool_pre_ping": True,
|
||||
"pool_recycle": 280, # Good practice to include this with pre-ping
|
||||
}
|
||||
|
||||
|
||||
db = SQLAlchemy(app) # create the (flask) sqlalchemy connection
|
||||
ma = Marshmallow(app) # set up Marshmallow - data marshalling / serialising
|
||||
|
||||
@@ -20,6 +20,7 @@ from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from contextlib import contextmanager
|
||||
|
||||
### LOCAL FILE IMPORTS ###
|
||||
from shared import DB_URL, PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, THUMBSIZE, SymlinkName, GenThumb, SECS_IN_A_DAY, PA_EXIF_ROTATER, PA
|
||||
@@ -66,21 +67,41 @@ override_tbls={ "face_no_match_override", "face_force_match_override", "disconne
|
||||
# this is required to handle the duplicate processing code
|
||||
sys.setrecursionlimit(50000)
|
||||
|
||||
# a Manager, which the Session will use for connection resources
|
||||
some_engine = create_engine(DB_URL)
|
||||
|
||||
# create a configured "Session" class
|
||||
#Session = sessionmaker(bind=some_engine)
|
||||
# 1. Add pool_pre_ping and pool_recycle here to handle db container disappearing underneath us
|
||||
some_engine = create_engine(
|
||||
DB_URL,
|
||||
pool_pre_ping=True, # check DB connection is still active before use
|
||||
pool_recycle=300, # churn connections regardless every 5 mins
|
||||
pool_size=20, # Parallel-ready base pool
|
||||
max_overflow=10 # Burst capacity for high socket traffic
|
||||
)
|
||||
|
||||
# create a Session
|
||||
session_factory = sessionmaker(bind=some_engine)
|
||||
Session = scoped_session(session_factory)
|
||||
session = Session()
|
||||
|
||||
# HACK: need to remove this and use 'sess' as an actual param everywhere, butt here are 200+ so quick fix until retired
|
||||
session = Session
|
||||
|
||||
# this is a way to handle a session failing
|
||||
@contextmanager
|
||||
def PA_db_session():
|
||||
"""Provide a transactional scope around a series of operations."""
|
||||
# This creates a NEW session from the registry
|
||||
s = Session()
|
||||
try:
|
||||
yield s
|
||||
s.commit()
|
||||
except Exception:
|
||||
s.rollback()
|
||||
raise
|
||||
finally:
|
||||
# This destroys the session and returns connection to pool
|
||||
Session.remove()
|
||||
|
||||
# this creates the Base (like db model in flask)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
################################################################################
|
||||
# Class describing PathType & in the database (via sqlalchemy)
|
||||
# series of pre-defined types of paths (import, storage, bin)
|
||||
@@ -1897,6 +1918,11 @@ def JobTransformImage(job):
|
||||
amt=[jex.value for jex in job.extra if jex.name == "amt"][0]
|
||||
e=session.query(Entry).join(File).filter(Entry.id==id).first()
|
||||
PAprint( f"JobTransformImage: job={job.id}, id={id}, amt={amt}" )
|
||||
# cant transfer non-image, but may get here if multi-select includes non-Image
|
||||
if e.type.name != 'Image':
|
||||
removeEntryAmendment( job, id )
|
||||
FinishJob(job, "Cannot rotate file as it is not an Image","Failed")
|
||||
return
|
||||
|
||||
if amt == "fliph":
|
||||
AddLogForJob(job, f"INFO: Flipping {e.FullPathOnFS()} horizontally" )
|
||||
@@ -1920,6 +1946,8 @@ def JobTransformImage(job):
|
||||
e.file_details.hash = md5( job, e )
|
||||
PAprint( f"JobTransformImage DONE thumb: job={job.id}, id={id}, amt={amt}" )
|
||||
session.add(e)
|
||||
# any faces in this file are no longer valid, remove them
|
||||
session.query(FaceFileLink).filter(FaceFileLink.file_eid==e.id).delete()
|
||||
removeEntryAmendment( job, id )
|
||||
|
||||
FinishJob(job, "Finished Processesing image rotation/flip")
|
||||
@@ -2744,7 +2772,13 @@ if __name__ == "__main__":
|
||||
|
||||
InitialValidationChecks()
|
||||
|
||||
HandleJobs(True)
|
||||
# Initial job run on startup (hence True in 1st param)
|
||||
try:
|
||||
with PA_db_session() as sess:
|
||||
HandleJobs(True)
|
||||
except Exception as e:
|
||||
PAprint(f"ERROR: Initial job handle failed: {e}")
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind((PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT))
|
||||
# force timeout every 1 day so we can run scheduled jobs
|
||||
@@ -2752,18 +2786,35 @@ if __name__ == "__main__":
|
||||
s.listen()
|
||||
while True:
|
||||
try:
|
||||
# 1. Wait for connection
|
||||
conn, addr = s.accept()
|
||||
if DEBUG:
|
||||
PAprint( f"accept finished, tout={s.timeout}" )
|
||||
PAprint(f"Connection accepted from {addr}")
|
||||
|
||||
# 2. Process Jobs after a successful socket connection
|
||||
with PA_db_session() as sess:
|
||||
HandleJobs(False)
|
||||
# Check for scheduled tasks as well
|
||||
if ScheduledJobs():
|
||||
HandleJobs(False)
|
||||
|
||||
except socket.timeout:
|
||||
if DEBUG:
|
||||
PAprint( f"timeout occurred, tout={s.timeout}" )
|
||||
if ScheduledJobs():
|
||||
HandleJobs(False)
|
||||
PAprint("Socket timeout (Daily maintenance window) reached.")
|
||||
|
||||
# 3. Process Scheduled Jobs during the timeout
|
||||
try:
|
||||
with PA_db_session() as sess:
|
||||
if ScheduledJobs():
|
||||
HandleJobs(False)
|
||||
except sqlalchemy.exc.OperationalError:
|
||||
PAprint("DB Connection lost during scheduled task window. Retrying next cycle.")
|
||||
continue
|
||||
else:
|
||||
HandleJobs(False)
|
||||
# in case we constantly have jobs running, the '1 day' last import might be missed, so check it after each job too
|
||||
if ScheduledJobs():
|
||||
HandleJobs(False)
|
||||
|
||||
except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.InterfaceError) as e:
|
||||
# This catches the DB container restart specifically
|
||||
PAprint(f"DATABASE ERROR: Connection lost. Retrying... {e}")
|
||||
time.sleep(5) # Brief pause before next socket listen
|
||||
|
||||
except Exception as e:
|
||||
PAprint(f"UNEXPECTED ERROR: {e}")
|
||||
|
||||
Reference in New Issue
Block a user